第5章 运算符,表达式和语句
5.1 循环简单
程序清单 5.1 显示了一个示例程序,该程序做了一点算术运算来计算穿 9 码鞋的脚用英寸表示的长度。为了增加你对循环的理解,程序的第一版演示了不使用循环编程的局限性。
程序清单 5.2 shoes2.c 程序
---------------------------------------------------------------------
/* shoes2.c ---- 计算多个鞋尺码对应的英寸长度 */
#include
#define ADJUST 7.64
#define SCALE 0.325
int main (void)
{
double shoe,foot;
printf (" Shoe size(men's) foot length \n");
shoe = 3.0 ;
while (shoe < 18.5) // while 循环仁开始
{
foot = SCALE * shoe + ADJUST;
printf ("%10.2f %15.2f inches \n",shoe,foot);
shoe = shoe + 1.0;
}
printf ("If the shoe fits,wear it.\n");
getchar();
return 0;
}
输出如下:
Shoe size(men's) foot length
3.00 8.62 inches
4.00 8.94 inches
5.00 9.27 inches
6.00 9.59 inches
7.00 9.91 inches
8.00 10.24 inches
9.00 10.57 inches
10.00 10.89 inches
11.00 11.22 inches
12.00 11.54 inches
13.00 11.87 inches
14.00 12.19 inches
15.00 12.52 inches
16.00 12.84 inches
17.00 13.16 inches
18.00 13.49 inches
If the shoe fits,wear it.
-----------------------------------------------------------------------------------
(顺便提一下,用于此转换的常量是在对鞋店的暗地访问时取得的。的尺码是针对男士鞋的尺码。那些对女士鞋的尺码感兴趣的人将不得不亲自去鞋店了。另外,该程序也帮了不太现实的假设,假设鞋的尺码有一个合理的统一系统)。解释一下 while 循环是如何工作的。当程序第一次到达 while 语句时,检查圆括号内的条件是否为真。在这个例子里,条件表达式是下面的式子:
shoe < 18.5
符号 < 的意思是 “小于”。变量 shoe被初始化为 3.0,它当然小于 18.5.所以,条件为真,程序继续执行下一个语句,将该尺码转换为英寸。然后程序打印结果。下一个语句将 shoe 增加 1.0 使
shoe 变成 4.0;
shoe = shoe + 1.0;
此时,程序返回 while 部分去检查条件。为什么在这点呢?因为后面是一个结束花括号(}),而代码使用一对花括号({})来标出 while 循环的范围。在两个花括号之间的语句是被重复执行的语句。花括号和在花括号里的程序部分被称为一个代码块(block)。现在回到程序中。值 4 小于 18.5,所以跟在 while 后的被括起来的全部命令(代码块)将被重复执行(在计算机术语中,称程序“循环”执行这些语句)。这个重复过程一直继续,直到 shoe 的值 达到 19.0 。
因为此时 19.0 不再小于 18.5 ,所以,下面的条件现在主变成了假:
shoe < 18.5
因而控制转到紧跟着 while 循环的第一个语句。在此例中,转到最后的 printf()语句。
你可以很容易的修改该程序用来做其他转换。例如,将 SCALE 变成 1.8, 将 ADJUST 变成32.0,你就有了一个将摄氏温度变成华氏温度的程序。将 SCALE 变成 0.6214,将 ADJUST 变成 0,就可以将公里转为英里。如果你做了这些改变,还应该更改打印的消息以防止引起迷惑。
while 循环提供了灵活方便的控制程序的方法。现在,我们讨论可以在程序中使用的各种基本运算符。
5.2 基本运算符
C 使用运算符(operator)来代表算术运算。例如,+ 运算符使在它两侧的值加在一起。如果术语“运算符”对你来说很奇怪,那么请你记住那些东西总得有个名称。与被称之为“那些东西”或“算术处理器”相比较,被称为“运算符”看起来的确是一个更好的选择。现在我们看一下用于基本算术运算的运算符:=,+,-,*,以及 / (C没有指数运算符。然而,标准 C 的数学库为此提供了一个 pow()函数。例如,pow(3.5,22)返回3.5的 2.2次幂)。
5.2.1 赋值运算符 : =
在 C 里,符号 = 不表示“相等”,而是一个赋值运算符。下面的语句将值 2002 赋给名字为 bmw 的变量 :
bmw = 2002;
也就是说,符号 = 左边是一个变量名,右边是赋给该变量的值。 符号 = 被称为赋值运算符(assignment opeartor)。再次强调不要把这行读为 “bmw 等于 2002”,而应该读为“将值 2002 赋给变量 bmw”。赋值运算符的动作是从右到左。
或许变量的名字和变量值之间的区别看起来微乎其微,但是请考虑下面的常用计算机语句:
i = i + 1;
在数学上,该语句没有任何意义。如果你给一个有限的数加 1,结果不会“等于”开始的那个数,但是作为计算机赋值的语句,它却是很合理的。它意味着“找到名字为 i 的变量的值,对那个值加 1,然后将这个新值赋给名字为 i 的变量”
像下面这样的语句:
2002 = bmw;
在 C 中是没有意义的(确切地说是无效的),原因是 2002 只是一个常量。你不能将一个值赋给一个常量;那个常量本身就是它的值了。所以,当你准备键入代码时请记住在符号 = 左边的项目必须是一个变量的名字。实际上,赋值运算符左边必须指向一个存储位置。最简单的方法是使用变量的名字,但是以后你会看到,‘指针’也可以用于指向一个存储位置。更普遍地,C 使用术语‘可修改的左值’(modifiable lvalue)来标志那些我们可以为之赋值的实体。“可修改的值”或许不是那么直观易懂,所以我们先看看一些定义。
几个术语: 数据对象,左值,右值和操作数
“数据对象”(data object)是泛指数据存储区的术语,数据存储区用于保存值。例如,用于保存变量或数组的数据存储区是一个数据对象。C 的术语左值(lvalue)指用于标识一个特定的数据对象的名字或表壳式。例如,变量的名字是一个左值。所以对象指的是实际的数据存储,但是左值是用于识别或定位那个存储的标识符。
因为不是所有的对象都是可更改值的,所以 C 使用术语“可修改的左值”来表示那些值可以被更改的对象。所以,赋值运算符的左边应该是一个可修改的左值。lvalue 中的 1 确实 来自于 left,因为可修改的左值可以用在赋值运算符的左边。
术语“右值”(rvalue)指的是赋给可修改的左值的量。例如,考虑下面的语句:
bmw = 2002;
这里 bmw 是一个可修改的值, 2002 是一个右值。你可能猜到 rvalue 里的 r 来自于 right 。右值可以是常量,变量或者任何可以产生一个值的表达式。
在你学习事物的名称时,我们称之为“项目”的东西(比如在“符号 = 左边的项目”中的“项目”)的正确术语是“操作数”(operand)。操作数是运算符操作的对象。例如,你可以把吃一个汉堡描述为用“吃”运算符操作‘汉堡’这个操作数。与之相似,你可以说 = 运算符的左操作数是可修改的值。
C 的基本赋值运算符有点与众不同。试一下程序清单 5.3 里的短程序。
程序清单 5.3 golf.c 程序
-------------------------------------------------------------
/* golf.c --- 高尔夫锦标赛记分卡 */
#include
int main (void)
{
int jane,tarzan,cheeta;
cheeta = tarzan = jane = 68;
printf (" cheeta tarzan jane \n");
printf ("first round score %4d %8d %8d \n",cheeta,tarzan,jane);
getchar();
return 0;
}
许多程序语言将在本程序的三重赋值处卡壳,但是 C 可以顺利接受它。赋值是从右到左进行的。首先 jane 得到值 68,然后 tarxan 得到值 68 ,最后 cheeta 得到值 68 。所以输出结果如下:
cheeta tarzan jane
first round score 68 68 68
------------------------------------------------------------------------------------
5.2.2 加法运算符 : +
“加法运算符”(addition operator)使得在它两侧的值被加在一起。例如,语句:
printf ("%d", 4 + 20);
将打印数 24 而不是打印表达式: 4 + 20
被加的值(操作数)可以是变量也可以是常量。 所以语句:
income = salary + bribes;
使计算机先查找右边的两个变量的值,将它们加起来,最后将这个和 赋给变量 income 。
-----------------------------------------------------------------------------------
5.2.3 减法运算符: -
“减法运算符”(subtraction operator)从它前面的数中减去它后面的数。例如,下面的语句将值 200.0 赋给 takehome :
takehome = 224.00 - 24.00;
+ 和 - 运算符被称为 二元(binary)运算符,这表示它们需要两个操作数。
----------------------------------------------------------------------------------
5.2.4 符号运算符: - 和 +
负号可以用于指示或改变一个值的代数符号。例如,下面的语句序列:
rocky = -12;
smokey = -rocky;
把值 12 赋给 smokey。
当这样使用负号时,称它为一元运算符(unary operator),表示它只需要一个操作数。
C90 标准将一元 + 运算符加进了 C 中。这个运算符不改变它的操作数的值或符号,它只是使你能使用像下面这样的语句:
dozen = + 12;
而不会得到编译器的报错信息。这种结构在以前是不允许的。
---------------------------------------------------------------------------------
5.2.5 乘法运算符: *
乘法由符号 * 表示。语句:
cm = 2.54 * inch;
用 2.54 乘以变量 inch,然后将结果赋给 cm。
有时候,你可能需要一个平方表。 C 没有计算平方的函数,但是正如程序清单 5.4所示,你可以使用乘法来计算平方。
程序清单 5.4 squares.c 程序
---------------------------------------------
/* squares.c --- 产生前 20个整数的平方表 */
#include
int main (void)
{
int num = 1;
while (num < 21)
{
printf ("%4d %6d \n",num,num*num);
num = num + 1;
}
getchar();
return 0;
}
正如你自己验证的那样,该程序了前 20个整数和它们的平方。我们看一个更为有趣的例子。
-------------------------------------------------
指数增长
你可能听说过这个故事,有一位强大的统治都想奖励一位对他做出突出贡献的学者。他问这位学者他想要什么,这位学者指着棋盘说,在第1个方格放1粒小麦,在第2个方格里放2粒,在第3个方格里放8粒,依此类推。由于缺乏丰富的数字知识,统治惊讶以此请求的谦逊,因为他原本准备奖励很大一笔财产。如果程序清单 5.5 的运行结果所示,这显然是跟统治都开了一个玩笑。它计算了每个方格里应放多少粒小麦,并计算了总数。你可能对小麦的产量不是很熟悉,所以这个程序将这个总数与美国小麦年产量的粗略估计进行了比较。
程序清单 5.5 wheat.c 程序
---------------------------------------------------------
/* wheat.c -- 指数增长 */
#include
#define SQUARES 64 /* 棋盘上的方格数 */
#define CROP 1E15 /* 以粒计美国小麦产量 */
int main (void)
{
double current,total;
int count = 1;
printf ("square grains total ");
printf ("Eraction of \n");
printf (" added grain ");
printf (" Us total \n");
total = current = 1.0; // 开始是一粒
printf ("%4d %13.2e %12.2e %12.2e\n", count,current,total,total/CROP);
while ( count < SQUARES)
{
count = count + 1;
current = 2.0 * current; /* 下个方格的粒数加倍 */
total = total + current; /* 更新总数 */
printf ("%4d %13.2e %12.2e %12.2e\n",count,current,total,total/CROP);
}
printf (" That's all \n");
getchar();
return 0;
}
输出如下:
square grains total Eraction of
added grain Us total
1 1.00e+000 1.00e+000 1.00e-015
2 2.00e+000 3.00e+000 3.00e-015
3 4.00e+000 7.00e+000 7.00e-015
4 8.00e+000 1.50e+001 1.50e-014
5 1.60e+001 3.10e+001 3.10e-014
6 3.20e+001 6.30e+001 6.30e-014
7 6.40e+001 1.27e+002 1.27e-013
8 1.28e+002 2.55e+002 2.55e-013
9 2.56e+002 5.11e+002 5.11e-013
10 5.12e+002 1.02e+003 1.02e-012
11 1.02e+003 2.05e+003 2.05e-012
12 2.05e+003 4.10e+003 4.09e-012
13 4.10e+003 8.19e+003 8.19e-012
14 8.19e+003 1.64e+004 1.64e-011
15 1.64e+004 3.28e+004 3.28e-011
16 3.28e+004 6.55e+004 6.55e-011
17 6.55e+004 1.31e+005 1.31e-010
18 1.31e+005 2.62e+005 2.62e-010
19 2.62e+005 5.24e+005 5.24e-010
20 5.24e+005 1.05e+006 1.05e-009
21 1.05e+006 2.10e+006 2.10e-009
22 2.10e+006 4.19e+006 4.19e-009
23 4.19e+006 8.39e+006 8.39e-009
24 8.39e+006 1.68e+007 1.68e-008
25 1.68e+007 3.36e+007 3.36e-008
26 3.36e+007 6.71e+007 6.71e-008
27 6.71e+007 1.34e+008 1.34e-007
28 1.34e+008 2.68e+008 2.68e-007
29 2.68e+008 5.37e+008 5.37e-007
30 5.37e+008 1.07e+009 1.07e-006
31 1.07e+009 2.15e+009 2.15e-006
32 2.15e+009 4.29e+009 4.29e-006
33 4.29e+009 8.59e+009 8.59e-006
34 8.59e+009 1.72e+010 1.72e-005
35 1.72e+010 3.44e+010 3.44e-005
36 3.44e+010 6.87e+010 6.87e-005
37 6.87e+010 1.37e+011 1.37e-004
38 1.37e+011 2.75e+011 2.75e-004
39 2.75e+011 5.50e+011 5.50e-004
40 5.50e+011 1.10e+012 1.10e-003
41 1.10e+012 2.20e+012 2.20e-003
42 2.20e+012 4.40e+012 4.40e-003
43 4.40e+012 8.80e+012 8.80e-003
44 8.80e+012 1.76e+013 1.76e-002
45 1.76e+013 3.52e+013 3.52e-002
46 3.52e+013 7.04e+013 7.04e-002
47 7.04e+013 1.41e+014 1.41e-001
48 1.41e+014 2.81e+014 2.81e-001
49 2.81e+014 5.63e+014 5.63e-001
50 5.63e+014 1.13e+015 1.13e+000
51 1.13e+015 2.25e+015 2.25e+000
52 2.25e+015 4.50e+015 4.50e+000
53 4.50e+015 9.01e+015 9.01e+000
54 9.01e+015 1.80e+016 1.80e+001
55 1.80e+016 3.60e+016 3.60e+001
56 3.60e+016 7.21e+016 7.21e+001
57 7.21e+016 1.44e+017 1.44e+002
58 1.44e+017 2.88e+017 2.88e+002
59 2.88e+017 5.76e+017 5.76e+002
60 5.76e+017 1.15e+018 1.15e+003
61 1.15e+018 2.31e+018 2.31e+003
62 2.31e+018 4.61e+018 4.61e+003
63 4.61e+018 9.22e+018 9.22e+003
64 9.22e+018 1.84e+019 1.84e+004
That's all
----------------------------------------------------------------------
5.2.6 除法运算符: /
C 使用符号 / 表示除法。 / 左边的值被它右边的值除。例如,下面的语句把值 4.0 赋给four:
four = 12.0 / 3.0;
整型数的除法运算和浮点型数的除法运算有很大的不同。浮点类型的除法运算得出一个浮点数结果,而整数除法运算则产生一个整数结果。整数不能有小数部分,这使得用 3 去除 5 很让人头痛,因为结果有小数部分。 在 C 中,整数除法结果的小数部分都被丢弃。这个过程被称为 截尾(truncation)
试一下程序清单 5.6 中的程序,看看截性如何进行,以及整数除法与浮点数除法有什么不同。
程序清单 5.6 divide.c 程序
------------------------------------------------------
/* divide.c ---- 我们所知的除法 */
#include
int main (void)
{
printf ("integer division : 5/4 is %d \n", 5/4);
printf ("integer division : 6/3 is %d \n", 6/3);
printf ("integer division : 7/4 is %d \n", 7/4);
printf ("floating division : 7./4. is %1.2f \n",7./4.);
printf ("mixed division : 7./4 is %1.2f \n",7./4);
getchar();
return 0;
}
程序清单 5.6 包括了一个“混合类型”的实例;用一个整数值去除一个浮点类型的值。C 是比较宽容的语言,它允许你这样做,但是正常情况下你应该避免混合类型。输出结果如下:
integer division : 5/4 is 1
integer division : 6/3 is 2
integer division : 7/4 is 1
floating division : 7./4. is 1.75
mixed division : 7./4 is 1.75
注意,没有把整数除法运算的结果四舍五入到最近的整数,而是进行截性,即舍弃小数部分。当你对整数与浮点数进行混合运算时,结果是浮点数。实际上,计算机不能真正用整数去除浮点数,所以编译器将两个操作数转变成一致的类型。在这种情况下,做除法运算之前将整数转化为浮点数。
C99 标准之前的 C语言给了实现者一些空间,让他们来决定对于负数整数除法如何工作。可以使用这样的方法,即舍入过程采用小于或等于该浮点数的最大整数。当然,3 相对于 3.8 而言是符合上面描述的。那么 -3.8呢?最大整数方法会建议将其四舍五入为 -4,因为 -4 小于 -3.8.但是另外一种舍入方法是简单地丢弃小数部分,这种称为“趋零截性”的解释方法建议将 -3.8 转换成 -3. 在 C99以前,不同实现采用不同的方法。但是 C99 要求使用“趋零截尾”所以应把 -3.8 转换成 -3
整数除法的属性在处理某些问题时是很方便的。很快你就会看到一个例子。首先,还有另一个重要的事情:当你在一个语句中进行多种运算时将发生什么?这就是下面要讨论的问题。
----------------------------------------------------
5.2.7 运算符的优先级
考虑下面的代码行:
butter = 25.0 + 60.0 * n / SCALE;
此语法有一个加法运算,一个乘法运算和一个除法运算。哪个运算会先发生?是25.0先加到60.0,然后用 n 乘以 前面的结果 85.0,最后将得到的结果用 SCALE 来除吗?还是先用 n 乘以 60.0,得到的结果加上 25.0,最后再用 SCALE 去队前面得到的结果?还是其他顺序? 我们取 n 为6.0, SCALE 为2.0 。如果你使用这些值对这个语句进行计算,你将发现第一个方法得到 255 ,第二个方法 得到 192.5。 C程序必定采用了某种其他顺序,因为该程序给出 butter 的值为 205.0.
显然,执行各种操作的顺序很重要, 所以 C需要关于执行顺序的明确规则。 C 通过建立一个运算符优先顺序来满足上述需求。将一个优先级赋予每个运算符。正像在普通的算术运算中那样,乘法和除法具有比加法和减法更高的优先级,所以先执行乘法和除法运算。如果两个运算符有相同的优先级将会发生什么?如果它们共享一个操作数,会根据它们在语句里出现的顺序执行它们。对于大多数的运算符,该顺序是从左到右的 (= 运算符是这个规则的例外)。所以,在下面的语句中:
butter = 25.0 + 60.0 * n / SCALE;
运算顺序如表 5.1 所示:
表 5.1 运算顺序
--------------------------------------------------------------------------
60.0*n 表达式里的第一个*或/(假设n的值为6,所以60.0*6的结果为360.0)
---------------------------------------------------------------------------
360.0/SCALE 然后,表达式里的第二个 * 或 / SCALE的值是2.0 结果 180.0
----------------------------------------------------------------------------
25.0+180 最后,表达式里的第一个+或-的结果为 205.0
-------------------------------------------------------------------------
如何让加法在除法之间执行?你可以像下面这样:
flour = (25.0 + 60.0 * n) / SCALE;
最先被执行的是圆括号包含的部分。在圆括号内部,运算按正常的规则进行。在本例中,先执行乘法运算,然后是加法,圆括号内的表达式就是如此完成的。最后才用 SCALE 去除这个结果。
表 5.2 总结了迄今为止用到的运算符的规则 (本书的封三包括了涉及到的所有运算符的表)。
表 5.2 按优先级递减顺序排列的运算符
-------------------------------------------------------------------------
运算符 结合性
------------------------------------------------------------------------
() 从左到右
-----------------------------------------------------------------------
+-(一元运算符) 从右到左
----------------------------------------------------------------------
*/ 从左到右
---------------------------------------------------------------------
+-(二元运算符) 从左到右
-----------------------------------------------------
= 从右到左
----------------------------------------------------
注意减号的两种用法具有不同的优先级,加号的两种用法也是如此。结合性那一列指出运算符如何与其他操作数相结合。例如,一元减号与它右边的量相结合,在除法中用右边的操作数去除左边的操作数。
5.2.8 优先级和求值顺序
运算符的优先级为决定表达式里求值的顺序提供了重要的规则,但是它并不决定所有的顺序。C 留下了一些由实现者自己来决定的选择。考虑下面的语句:
y = 6 * 12 + 5 * 20;
当两个运算符共享一个操作数时,优先级规定了求值的顺序。例如,12既是 * 运算符的操作数又是 + 运算符的操作数,根据优先级的规定乘法运算先进行。与之相似,优先级规定了对 5 进行乘法操作而不是加法操作。总之,两个乘法运算 6 *12 和 5 * 20 在加法运算之前进行。优先级没有确定的是这两个乘法运算中到底哪个先进行。 C 将这个选择权留给实现者,这是因为可能一种选择在一种硬件上效率更高,不管先执行哪个乘法运算,表达式都会简化成 72 + 100,所以对于这个具体的例子,这个选择不影响最终的结果。你说“但是乘法的结合是从左到右的顺序。难道不意味着先执行最左边的乘法吗”?(可能你没有那样说,但是有人可能会这么说),结合规则适用于共享同一操作数的运算符。例如,在表达式 12/3*2里,有相同优先级的 / 和 * 运算符共享操作数 3,所以,从左到右的原则适用于这个例子。这个表达式将被简化为 4*2即8(如果是从右到左计算,结果将是 12/6,即2, 这种选择对该例产生了影响)。在前面的例子中,两个 * 运算符不共享一个操作数,所以,从左到右的规则对它并不适用。
试验一下规则
我们用更复杂的例子来试一下这些规则,请看程序清单 5.7
程序清单 5.7 rules.c 程序
-----------------------------------------------------------------------
/* rules.c -- 优先级规则的试验 */
#include
int main (void)
{
int top,score;
top = score = - (2+5)*6+(4+3*(2+3));
printf (" top = %d \n",top);
getchar()
return 0;
}
这个程序将打印什么值?猜测一下,然后运行该程序或阅读下面的描述来检查你的答案。
首先,圆括号有最高的优先级。先计算-(2+5)*6 里的圆括号,还是先计算(4+3*(2+3))里的圆括号?像我们刚刚讨论的过的那样,这有赖于不同的实现。在本例中每个选择都将导致同样的结果,所以我们先执行左面的计算。圆括号的高优先级意味着在子表达式 -(2+5)*6中,你先计算(2+5)并得到了7,下一步,你对7应用一元负运算得到了 -7.现在这个表达式变为:
top = scoe = -7*6+(4+3*(2+3))
下一步是计算 2+3的值。表达式变为:
top = scoe = -7*6+(4+3*5)
下一步,因为圆括号里 * 又高于 + 的会优先级,所以表达式变成:
top = score = -7 * 6 + (4+15)
然后:
top = score = -7 *6 +19
用 6 乘以 -7 得到下面的表达式:
top = score = -42 +19
然后用加法运算,得到:
top = score = -23
现在 score 赋值为 -23 。 最后, top得到值-23 。记住 = 运算符的结合是从右到械的。
5.3 其他运算符
C 有大约 40个运算符,其中有些运算符比其他运算符要常用得多。我们已经讨论过的那些是最常用的,现在我们接着介绍 4 个比较有用的运算符。
5.3.1 sizeof 运算符和 size_t 类型
在第 3章“数据和C”中,你看到了 sizeof运算符。回顾一下,sizeof 运算符以字节为单位返回其操作数的大小(在 C中,1个字节被定义为 char 类型所占用空间的大小。在过去,1 个字节通常是 8位,但是一个字符集可能使用更大的字节)。操作数可以是一个具体的数据对象(例如一个变量名),或者一个类型。如果它是一个类型(如 float),操作数必须被括在圆括号里。程序清单 5.8 的例子演示了这两种形式的用法。
程序清单 5.8 sizeof.c 程序
-----------------------------------------------------------------------------
/* sizeof.c 使用 sizeof 运算符 */
#include
int main (void)
{
int n = 0;
size_t intsize;
intsize = sizeof(int);
printf (" n=%d,nhas %zd bytes:all ints have %zd bytes.\n",n,sizeof n,intsize);
getchar();
return 0;
}
C 规定 sizeof返回 size_t类型的值。这是一个无符号整数类型,但它不是一个新类型。相反,与可移植类型(如 int32_t等)相同,它是根据标准类型定义的。C 有一个 typedef 机制(在 14章“结构和其他数据形式”中将对此做一步讨论),它允许你为一个已有的类型创建一个别名。例如:
typedef double real;
使 real 为成 double 的别名。现在你可以声明一个 real 类型的变量:
real deal; // 使用由 typedef 定义的类型
编译器将会看到单词 real,回想起 typedef 语句把 real定义为 double 的别名,于是它把 deal创建为一个 double 类型的变量。与此相似, C 的头文件系统可以使用 typedef 来使 size_t 在系统中作为 unsigned int 或 unsigned long 的同义词。这样当你使用 size_t时,编译器会用适合你的系统的标准类型代替之。
C99更进一步,把 %zd 作为用来显示 size_t 类型值的 printf()说明符。如果你的系统没有实现 %zd,你可以试着使用 %u 或 %lu 代替它。
5.3.2 取模运算符 %
取模运算符(modulus operator)用于整数运算。该运算符计算出用它右边的整数去除它左边的整数得到的余数。例如,13%5(读作“对13除以5取模”)所得值为 3,因为 13除以 5 得 2并余 3.不要对浮点数使用该运算符,那将是无效的。
乍一看,你可能认为这个运算符是数学家使用的深奥工具,但是它实际上相当实用。一个常见的用途是帮助你控制程序的流程。例如,假设你正在编写一个预算账单的程序,该程序被设计为每三个月就加进一笔额外的费用,只需让程序对月份进行除以 3的取模操作(即,month %3)并检查结果是否为 0 。如果是,程序就加进额外的费用。学习了第 7章“C控制语句:分支和跳转”中的 if 语句后,你对此将会有更好的理解。
程序清单 5.9 min_sec.c 程序
----------------------------------------------------------------------
/* min_sec.c --- 把秒转换为分钟和秒 */
#include
#define SEC_PER_MIN 60 // 每分钟的秒数
int main (void)
{
int sec,min,left;
printf ("Convert seconds to minutes and seconds!\n");
printf ("Enter the number of seconds (<=0 to quit):\n");
scanf ("%d",&sec); // 读入秒数
while (sec > 0)
{
min = sec / SEC_PER_MIN; //截尾后得到的分钟数
left = sec % SEC_PER_MIN; //剩下的秒数
printf ("%d seconds is %d minutes,%d seconds,\n",sec,min,left);
printf ("Enter next value (<=0 to quit):\n");
scanf ("%d",&sec);
}
printf ("Done!\n");
getchar();
getchar();
return 0;
}
下面是一个输出例子:
Convert seconds to minutes and seconds!
Enter the number of seconds (<=0 to quit):
3652
3652 seconds is 60 minutes,52 seconds,
Enter next value (<=0 to quit):
0
Done!
程序清单 5.2 使用一个计数器来控制 while 循环。当计数器超出给定的大小,循环终止。而程序清单5.9则使用 scanf()来获得一个新的值赋给变量 sec 。只要这个变量是正数,循环就会继续。当用户输入 0 或者一个负值的时候,循环就会停止。两种情况中同样重要的一点在于,每次循环都会修改被测试的变量的值。
负数的取模运算应遵照什么规则?在 C99为整数除法规定“趋零截尾”规则之前,该问题的处理方法有很多可能。但有了这条规则之后,如果第一个操作数为负数,那么得到的模也为负数,如果第一个操作数为正数,那么得到的模也正数;
11 / 5 is 2 and 11 % 5 is 1
11 / -5 is -2 and 11 % -2 is 1
-11 / -5 is 2 and -11 % -5 is -1
-11 / 5 is -2 and -11 % 5 is -1
如果你的系统有不同的行为,那么它还没有遵循 C99标准。该标准实际上规定:不管在什么情况下,如果 a 和 b 都是整数值,你可以通过从 a 中减去(a/b) * b 来计算 a%b。例如,你可以像这样来计算 -11%5 的值:
-11 -(-11/5)* 5 = -11 - (-2)* 5 = -11 - (-10) = -1
5.3.3 增量和减量运算符: ++ 和 --
“增量运算符”(increment operator)完成简单的任务,即将其操作数的值增加1 。空上运算符以两种方式出现。在第一种方式中,++ 出现在它作用的变量的前面,这是前缀(prefix)模式。在第二种方式中,++出现在它作用的变量后面,这是后缀(postrix)模式。这两种模式的区别在于值的增加这一动作发生的准确时间是不同的。我们先解释它们的相似之处,然后再解释其区别。程序清单 5.10 中的简短例子说明了增量运算符是如何工作的。
程序清单 5.10 add_one.c 程序
----------------------------------------------------------------------------
/* add_one.c --- 增量:前缀和后缀 */
#include
int main (void)
{
int ultra =0, super =0;
while (super < 15)
{
super++;
++ultra;
printf ("super = %d,ultra =%d\n",super,ultra);
}
getchar();
getchar();
return 0;
}
输出结果如下:
super = 1,ultra =1
super = 2,ultra =2
super = 3,ultra =3
super = 4,ultra =4
super = 5,ultra =5
这个程序两次同时计数到 5.通过使用如下语句代替两个增量语句,你可以得到相同的结果:
super = super + 1;
ultra = ultra + 1;
这些是很简单的语句。为什么要不辞辛苦地创建两个缩写形式呢?一个原因是这种精简的形式使你的程序更为整洁,更易于阅读。这些运算符使你的程序看起来很美观,可以赏心悦目。例如,你可以如此重写 shoes.c (程序清单 5.2)中的一部分代码:
shoe = 3.0;
while (shoe <18.5)
{
foot = SCALE * size + ADJUST;
printf ("%10.1f %20.2f inches \n",shoe,foot);
++shoe;
}
然而,你仍末充分利用增量运算符的好处。你可以像下面这样缩短这段程序:
shoe = 2.0;
while (++shoe < 18.5)
{
foot = SCALE * shoe + ADJUST;
printf ("%10.1f %20.2f inches \n",shoe,foot);
}
这里你已经将增量的过程和 while 循环的比较合并成一个表达式。这种结构在 C中很普遍,所以值得我们进一步观察分析。
首先,这个结构是如何工作的?很简单。shoe的值增加 1,然后与 18.5进行比较。如果小于 18.5,花括号里的语句将被执行一次。然后 shoe再次增加 1,并且重复这个循环,直到 shoe变得太大了为止。将 shoe的初始值从3.0 改为 2.0来补偿在第一个foot计算以前被增加的 shoe 。
第二,这种方法有什么好处?它更简洁。更重要的是它在一个地方集中了控制循环的两个处理过程。主要处理过程是判断是否继续循环。在本例中,判断是检查看看鞋子的尺码是否小于 18.5 。附带的处理过程是改变判断的元素:在本例中,是增加鞋子的尺码。
假设你忘了改变鞋子的尺码,于是 shoe将总是小于 18.5,循环将永不结束。计算机陷于一个无限循环 (infinite loop)中,产生一行行相同的东西。最后,你只能无奈地以某种方式强行关闭这个程序。在同一个位置执行循环的判断和循环的改变可以防止你忘记更新循环。
缺点是将两个运算合并到一个单一的表达式里将使代码难于理解,并易于产生计数错误。
增量运算符的另一个优点是它通常产生更高效的机器语言代码,因为它与实际的机器语言指令相似。然而,随着商家推出更好的 C 编译器,这个好处可能会消失。一个智能编译器能识别出 x=x+1,并把它与 ++x相同对待。
最后,这两个运算符还有另外一个特性,有时在某些微妙的场合这个特性很有用。为了发现这个特性,试着运行在程序清单 5.11里的程序。
程序清单 5.11 post_pre .c 程序
---------------------------------------------------------------
/* post_pre.c ---- 后缀和前缀 */
#include
int main (void)
{
int a = 1, b = 1;
int aplus,plusb;
aplus = a++; //后缀
plusb = ++b; //前缀
printf ("a aplus b plusb \n");
printf ("%ld %5d %5d %5d \n",a,aplus,b,plusb);
getchar();
return 0;
}
如果你和你的编译器都正确无误地做了每一件事,那么你将得到下列结果:
a aplus b plusb
2 1 2 2
像我们所能预料到的,a 和 b 都增加了 1 。然而,aplus 具有 a 改变之前的值,而 plusb 具有 b 改变之后的值。这就是前缀形式和后缀形式的不同之处(请参见图5.5)
图5.5
---------------------------------------------------------------------------------
q = 2 * ++a ;(前缀) /* 首先,a 增加 1 然后,用 2 乘以 a 并将结果赋给 q */
q = 2 * a++; (后缀) /* 首先,用 2 乘以 a 并将结果赋给 q ,然后, a 增加 1 */
---------------------------------------------------------------------------------
aplus = a++; /* 后缀:使用 a 的值之后改变 a */
plusb = ++b; /* 前缀:使用 b 的值之前改变 b */
当单独使用这些增量运算符之一时(像在一个独立的语句 ego++;中那样),你使用哪种形式无关紧要。然而当运算符及其操作数是一个更大的表达式的一部分时,比方说在你刚才看到的赋值语句中,选择就很重要了。在这种情况下,你必须考虑你想要的结果。例如,回忆一下我们曾建议使用下面的代码:
while (++shoe < 18.5>)
这个判断条件提供了一个尺码直到到 18 的表。如果你使用 shoe++ 而不是 ++shoe,这个表将达到尺码19,因为 shoe 将在比较之后而不是之前增加。当然,你可以仍然使用下面这种不太精致的形式:
shoe = shoe + 1;
但是没人会相信你是一个真正的 C程序员。
当你读这本书的时候,你应该更加留意增量运算符的例子。自问一下你是否能互换地使用前缀和后缀形式,或者是否环境决定了必须使用某个特定的选择。
也许一个更为明智的原则是避免那种前缀形式和后缀形式将导致不同效果的代码。例如,不要使用下列语句:
b = ++ i; // 如果使用 i++, b 将会有不同的结果
而是使用下列语句来代替它:
++i; //第一行
b = i; // 如果是第一行使用了 i++, b的结果仍会是相同的
然而,有时不那么小心翼翼会更有趣。所以本书将不总是遵循这个明智的建议。
5.3.4 减量: --
每种形式的增量运算符都有一种形式的减量运算符(decrement operator)与之对应,只须使用--来代替 ++:
-- count; // 减量运算符的前缀形式
count -- ; // 减量运算符的后缀形式
程序清单 5.12 说明了计算机可以是位熟练的抒情诗人。
程序清单 5.12 bottles.c 程序
---------------------------------------------------------------
#include
#define MAX 100
int main (void)
{
int count = MAX + 1;
while (--count > 0)
{
printf ("%d bottles of spring water on the wall"
"%d bottles of spring water \n",count,count);
printf ("take one down and pass it around,\n");
printf ("%d bottles of spring water\n\n",count-1);
}
getchar();
return 0;
}
其输出如下形式开始;
100 bottles of spring water on the wall 100 bottles of spring water
take one down and pass it around,
99 bottles of spring water
99 bottles of spring water on the wall 99 bottles of spring water
take one down and pass it around,
98 bottles of spring water
然后,输出继续进行,最后以如下方式结束:
2 bottles of spring water on the wall 2 bottles of spring water
take one down and pass it around,
1 bottles of spring water
1 bottles of spring water on the wall 1 bottles of spring water
take one down and pass it around,
0 bottles of spring water
显然,这位熟练的抒情诗人在复数的表达上有点问题,但是这可以通过使用第 7章“C控制语句:分支与跳转”里的条件运算符来解决。
顺便提一下,> 运算符 代表“大于”。与 < “小于”相似,它是一个关系运算符(relational operator)。在第 6章“C控制语句:循环”中你将会更深入地了解关系运算符。
5.3.5 优先级
增量和减量运算符有很高的结合优先级,只有圆括号比它们的优先级高。所以,x*y++ 代表 (x)*(y++) 而不是 (x*y)++ 。幸亏后者无效,增量运算符和减量运算符只能影响一个变量(或者更一般地讲,一个可修改的左值),而组合 x*y 本身不是一个变量,尽管它的各个部分是变量。
不要将这个运算符的优先级和求值的顺序相混淆。假设你有下列代码:
y = 2;
n = 3;
nextnum = (y + n++)* 6;
nextnum 的值是什么?用值来代替变量可以得到:
nextunm = (2+3)*6 = 5*6 = 30
只有当使用了 n 之后,n 的值才增加到 4 。优先级告诉我们 ++ 只属于 n,而不属于 y+n 。它也告诉我们什么时候使用 n 的值计算表达式,而增量运算符的性质决定了什么时候改变 n 的值。
当 n++ 是表达式的一部分时,你可以认为它表示 “先使用 n ,然后将它的值增加”
当 ++n 是表达式的一部分时,你可以认为它表示“先将 n 的值增加,然后再使用它”。
5.3.6 不要太聪明
如果你企图一次使用太多的增量运算符,可能连自己都会弄糊涂。例如,你可能认为你可以改进 squares.c程序(程序清单5.4),方法是使用下面的代码代替 while 循环来打印整数和它们的平方:
while (num <21>)
{
printf ("%10d %10d\n",num,num*num++);
}
这看起来是合理的。你打印数值 num,然后用它本身来乘它以得到平方值,最后将 num 增加 1.事实上,这个程序可能只在某些系统上可以正常工作,但不是所有的系统上都可以。问题是当 printf()获取要打印的值时,它可能先计算最后一个参数的值,从而计算其他参数之前增加 num 的值。所以,不是打印成:
5 25
而是可能打印成:
6 25
在 C中,编译器可以选择先计算函数里哪个参数的值。这个自由提高了编译器的效率,但是如果在函数参数里使用了增量运算符就会带来麻烦。
另一个麻烦的可能来源是像这样的语句:
ans = num / 2 + 5(1 + num++);
问题依然是编译器可能不以你想象的顺序来操作。你可能认为编译器应该先找到 num/2, 然后继续进行,但是它可能先做最后的项目,即先增加 num 的值,然后在 num/2 中使用新值。这些都是没有保证的。
另一个麻烦的例子如下:
n = 3;
y = n++ + n++;
当然在该语句被执行后,n 的值比以前的大 2,但是 y 的值是不确定的。一个编译器可能在计算 y 值时使用 n 的旧值两次,然后将 n 增加两次。这使 y 的值为 6, n 的值为 5 。 或者编译器使用 n 的旧值一次,然后增加 n 的值一次,在表达式里再使用第二个 n 值,最后第二次增加 n 的值。这种方法使 y 的值为 7,n 的值为 5 。两种选择都是允许的。更准确地说,这个结果是不确定的,这意味着 C 标准没有定义结果将是什么。
通过如下原则,你可以很容易地避免这些问题:
·如果一个变量出现在同一个函数的多个参数中时,不要将增量或者减量运算符用于它上面。
·当一个变量多次出现在一个表达式里时,不要将增量或减量运算符运用到它的上面。
另一方面,关于什么时候执行增量动作, C 还是做出了一些保证的。我们在本章稍后的“副作用和顺序点”部分讨论到顺序点时会回到这个主题。
5.4 表达式和语句
我们已经在前几章里多次使用了术语表达式和语句,现在该进一步学习它们的意思了。语句组成了 C 的基本的程序步骤,并且大多数语句由表达式构造而成的。这一事实建议你先了解一下表达式。
5.4.1 表达式
表达式(expression)是由运算符和操作数组合构成的(回忆一下,操作数是运算符操作的对象)。最简单的表达式是一个单独的操作数,以此作为基础可以建立复杂的表达式。下面是一些表达式:
4
-6
4+21
a*(b+c/d)/20
q = 5*2
x = ++q % 3
q > 3
正如你所看到的,操作数可以是常量,变量或者是二者的组合。一些表达式是多个较小的表达式的组合,这些小的表达式被称为子表达式(subexpression)。例如,c/d 是第4个例子的子表达式。
每一个表达式都有一个值
C 的一个重要的属性是每一个 C表达式都有一个值。为了得到这个值,你可以按照运算符优先级描述的顺序来完成运算。我们所列出的前几个表达式的值很明显,但是有 = 的表达式的值是什么呢?那些表达式与 = 左边的变量取得的值相同。所以,表达式 q=5*2 作为一个整体的值为 10. 表达式 q>3呢?这样的关系表达式如果条件为真取得的值为 1,如果条件为假取得的值为 0. 表5.3中是一些表达式和它们的值:
表 5.3 一些表达式和它们的值
----------------------------------------------------------------------
表达式 值
----------------------------------------------------------
-4+6 2
------------------------------------------------------
c = 3+8 11
-------------------------------------------------
5 > 3 1
------------------------------------------------
6+ (c=3+8) 17
------------------------------------------------
最后的表达式有点奇怪!然而,它在 C 中是完全合法的(但不建设使用),因为它是两个子表达式的和,每一个子表达式都有一个值。
5.4.2 语句
语句(statement)是构造程序的基本成分。程序(program)是一系列带有某种必需的标点的语句集合。一个语句是一条完整的计算机指令。在 C 中,语句用结束处的一个分号标识。所以:
legs = 4
只是一个表达式(它可能是一个较大语句的一部分) 而:
legs = 4;
是一个语句。
什么构成了一条完整的指令?首先, C 把任何后面加有一个分号的表达式看作是一个语句(它们被称为表达式语句)。所以,C 将反对像下面的各行:
8;
3 + 4;
但是这些语句对你的程序不做任何事情,不能被认为是有作用的语句。更典型的,语句会改变值和调用函数:
x = 25;
++x;
y = sqtr(x);
尽管一个语句(或者至少是一个有作用的语句)是一条完整的指令,但不是所有的完整的指令都是语句。考虑下面的语句:
x = 6 + (y = 5);
在此语句中,子表达式 y = 5 是一个完整的指令,但是它只是一个语句的一部分。因为一条完全的指令不必是一个语句,所以分号被用来识别确实是语句的指令。
到目前为止,你已经遇到了 4种语句。程序清单 5.13 给出了使用 这 4种语句的简短例子。
程序清单 5.13 addemup.c 程序
------------------------------------------------------------------
/* addemup.c ---- 4种类型的语句 */
#include
int main (void) // 求出前 20 个整数的和
{
int count,sum; // 声明语句
count = 0; // 赋值语句
sum = 0;
while (count++ < 20) // while 语句
sum = sum + count;
printf ("sum = %d \n",sum); // 函数语句
getchar();
return 0;
}
我们讨论程序清单 5.13 。此时,你一定很熟悉声明语句了。但是,我们将提醒你它用于建立变量的名字和类型并导致为它们分配内存空间。注意一个声明语句不是一个表达式语句。也就是说,如果你从声明里去掉分号,你所得的不是一个表达式,也不具有一个值:
int prot /* 不是一个表达式,也没有值 */
赋值语句(assignment statement)被许多程序广为使用:它为一个变量分配一个值。它的结构是一个变量名,变量后面紧跟着一个赋值运算符,赋值运算符后跟一个表达式,表达式后面跟上一个分号。注意在 while 循环语句中包含了一个赋值语句。赋值语句是表达式语句的特例。
函数语句(function statement)引起函数的执行。在这个例子里,调用了 printf()函数来打印结果。 while 语句有三个不同的部分请参见图 5.6
图 5.6
|
| while
假 |
<------------------(判断条件)--------------
转到下一个语句 | |
| 真 循环 |
| |
printf("be my valentine \n");-------
首先是关键字 while ,然后是在圆括号里的一个判断条件,最后是满足判断条件时将执行的语句。包含在循环里的可以是一个像本例中那样简单的语句,这个简单语句不需要用花括号来划分出来;也可以像以前的例子中那样是一个复合语句,复合语句需要有花括号。后面马上就要讨论有关复合语句的内容。
while 语句属于一类有时被称为 “结构化语句”(structured statement)的语句,因为它们的结构比一个简单的赋值语句复杂。在后面的章节里,我们将遇到许多其他的结构化语句。
副作用和顺序点
现在我们再讨论一些 C 的术语。副作用(side effect)是对数据对象或文件的修改。例如,语句:
states = 50;
的副作用是将变量 states 的值设置为 50 。这是副作用?这看起来更像是主要目的!然而,从 C的角度来看,主要目的是对表达式求值。给 C 一个表达式 4+6, C 将计算它的值为 10 。给 C 一个表达式式 states = 50,C 将计算它的值为 50 。计算这个表达式的副作用就是把变量 states 的值改变为 50 。跟赋值运算符一样,增量运算符和减量运算符也有副作用,它们主要由于副作用而被使用。
一个顺序点(stquence point)是程序执行中的一点,在该点处,所有副作用都在进入下一步前被计算。在 C 中,语句里的分号标志了一个顺序点。它意味着一个语句中赋值运算符,增量运算符及减量运算符所做的全部改变必须在程序进入下一个语句前发生。在后续的章节中我们将要讨论的一些运算符也有顺序点。任何一个完整的表达式的结束也是一个顺序点。
什么是完整的表达式呢?一个完整的表达式(full exprssion)是这样一个表达式--- 这不是一个更大的表达式的子表达式。完整的表达式的例子包括一个表达式语句里的表达式和在一个 while 循环里作为判断条件的表达式。
顺序点帮助阐明后缀增量动作何时发生。例如,考虑下面的代码:
while (guests++ < 10)
printf ("%d \n",guests);
有时 C 的初学都会设想在本程序中“先使用该值,然后增加它的值”的意思是在使用 printf()语句后再增加 guests 的值。然而,因为 guests++ < 10 是 while 循环的判断条件,所以它是一个完整表达式,这个表达式的结束就是一个顺序点。因此,C 保证副作用(增加 guests 的值)在程序进入 printf()前发生。同时使用后缀形式保证了 guests 在与 10 比较后才增加。
现在考虑这个语句:
y = (4 + x++) + (6 + x++);
表达式 4 + x++ 不是一个完整表达式,所以 C 不能保证在计算子表达式 4 + x++ 后立即增加 x。这里,完整表达式是整个赋值语句,并且分号标记了顺序点,所以 C 能保证的是在程序进入后续语句前 x 将被增加 两次。 C 没有指明 x 是在每个子表达式被计算后增加还是在整个表达式被计算后增加,这就是我们要避免使用这类语句的原因。
5.3.4 复合语句 (代码块)
复合语句(compound statement)是使用花括号组织起来的两个或更多的语句,它也被称为一个代码块(block)。shoes2.c程序使用一个代码块以使 while 语句包含多个语句。比较下面的两个程序段:
/* 程序段 1 */
index =0;
while (index++ < 10)
sam = 10 * index +2;
printf ("sam = %d\n",sam);
/* 程序段 2 */
index = 0;
while (index++ < 10)
{
asm = 10 * index + 2;
printf ("sam = %d\n",sam);
}
在程序段 1中,只有赋值语句包含在 while 循环中。在没有花括号的情况下,while循环语句的范围是从 while 到下一分号。printf()函数只在循环结束后被调用一次。
在程序段 2 中,花括号确保两个语句都是 while循环的一部分,所以每次循环执行时都调用 printf()。根据 while 语句的结构,整个复合语句被认为是一个语句。
PS: 风格提示
再看一下这两个 while 程序段,注意缩排是如何划分出每一个循环体的。缩排对编译不起作用,编译器使用花括号和 while 循环结构自身的知识来决定如何解释你的指令。缩排是为了使你一眼就可以看出这个程序是如何组织的。此例子显示了用花括号来指明一个代码块或复合语句的一种流行风格。另一个常用的风格是这样的:
while (index++ < 10){
sam = 10 * index + 2;
printf ("sam = %d \n",sam);
}
这个风格突出了该代码是附在 while 循环后面的。前面那种风格强调这些语句形成一个代码块。对于编译器而言,两种形式是相同的。总之,使用缩排可以为读者指明程序的结构。
总结: 表达式和语句
表达式:
表达式(expression)是运算符和操作数的组合。最简单的表达式只有一个常量或一个变量而没有运算符,例如 22 或者 beebop。更复杂的例子是 55+22 和 vap=2*(vip+(vup=4))。
语句:
语句(statement)是对计算机的命令。有简单语句和复合语句。简单语句(simple statement)以一个分号结束,如下面的这些例子:
声明语句: int toes;
赋值语句: toes = 12;
函数调用语句: printf ("%d \n",toes);
结构化语句: while (toes < 2)toes = toes + 2;
空语句: ; /* 什么也不做 */
复合语句(compound statement)或代码块(block)由一个或多个括在花括号里的语句(这些语句本身也可能是复合语句)构成。下面的 while 语句中含有一个例子:
while (years < 100)
{
wisdom = wisdom * 1.05;
printf ("%d %d \n",years,wisdom);
years = years + 1;
}
5.5 类型转换
语句和表达式通常应该只使用一种类型的变量和常量。然而,如果你混合使用类型,C 不会像 pascal 那样停在那里死掉。相反,它使用一个规则集合来自动完成类型转换。这可能很方便,但是它也很危险,是在你无意地混合使用类型的情况下(许多 UNIX 系统都有自带的 lint 程序可以检查类型“冲突”。如果你选择了一个更高的错误等级,许多非 UNIX 的 C 编译器将报告可能的类型问题)。你最好能有一些类型转换规则的知识。基本的规则如下:
1. 当出现在表达式里时,有符号和无符号的 char 和 short 类型都将自动被转换为 int ,在需要的情况下,将自动被转换为 unsigned int (如果 short 与 int 有相同的大小,那么 unsigned short 比 int 大;在那种情况下,将把 unsigned short 转换为 unsigned)。在 K&R C 下,但不是当前的 C 下,float将被自动转换为 double。因为是转换成较大的类型,所以这些转换被称为提升(promotion)。
2. 在包含两种数据类型的任何运算里,两个值都被转换成两种类型里较高的级别。
3. 类型级别从高到低的顺序是 long double, double, float, unsigned long long, long long, unsigned long, long, unsigned int 和 int 。一个可能的例外是当 long 和
int 具有相同大小时,此时 unsigned int 比 long 的级别更高。之所以 short 和 char 类型没有出现在此清单里,是因为它们已经被提升到 int 或也可能被提升到 unsigned int。
4. 在赋值语句里,计算的最后结果被转换成将要被赋予值的那个变量的类型。像规则 1 中一样,这个过程可能导致提升,但也可能导致阶级 (demotion),降级是将一个值转换成一个更低级的类型。
5. 当作为函数的参数被传递时,char 和 short 会被转换为 int ,float 会被转换为 double 。像在 第 9 章“函数”中讨论的那样,可以通过函数原型来阻止自动提升的发生。
提升通常是一个平滑的无损害的过程,但是降级可能导致真正的问题。原因很简单:一个较低级别的类型可能不够大,不能存放一个完整的数。一个 8 字节的 char 变量可以存放整数 101,但是不能存放整数 22334. 当把浮点类型降级为整数类型时,它们被趋零截尾或传入。这意味着 23.12 和 23.99 都被截尾成 23,-23.5 被截尾成 -23 。程序清单 5.14说明了这个规则。
程序清单 5.14 convert.c 程序
-----------------------------------------------------------------
/* convert.c -- 自动类型转换 */
#include
int main (void)
{
char ch;
int i;
float fl;
fl = i = ch ='C'; // 9
printf ("ch = %c, i = %d, fl = %2.2f \n",ch,i,fl); // 10
ch = ch + 1; // 11
i = fl + 2 * ch; // 12
fl = 2.0 * ch + i; // 13
printf ("ch = %c, i = %d, fl = %2.2f \n",ch,i,fl); // 14
ch = 5212205.17; // 15
printf ("Now ch = %c \n",ch);
getchar();
return 0;
}
下面是运行产生的结果:
ch = C, i = 67, fl = 67.00
ch = D, i = 203, fl = 339.00
Now ch = -
下面是在 8 位 char,32位 int 的系统上运行程序的过程分析:
· 第 9 行和第 10行:字符‘C’被作为 1字节的 ASCII 值存储在 ch 里。整数变量 i 接受由‘C’转换成的整数,即 67,它以 4个字节存储。最后,fl 接受由 67转换成的浮点数,即 67.00 。
· 第 11 行和第 14 行:字符变量‘C’被转换成整数 67,然后把该整数加 1.结果的 4字节的整数 68 被截为 1字节并存储在 ch 里。当使用 %c 说明符进行打印时,68 被解释为‘D’的ASCII码。
·第 12行和第 14行:为了和 2 相乘,ch 的值被转换为一个 4 字节的整数(68)。乘积整数(136)为了和 lf 相加而被转换为浮点类型。结果(203.00f)被转换成 int 类型并存储在 i 中。
·第 13行和第 14行:为了和 2.0相乘,ch 的值(‘D’,即68)被转换为浮点类型。为了做加法,i 的值(203)被转换为浮点类型。结果(339.00)被存储在 fl 中。
·第 15行和第 16行:在这里,示例程序尝试了一个降级的实例,把 ch 设置为一个很有大的数。在截去高位后,ch 最终变成连字符这一字符的 ASCII 码。
指派运算符
通常你应该避免自动类型转换,是避免降级。但是倘若你小心使用,有时候它对做类型转换很方便。到目前为止我们已经讨论的类型转换是自动完成的。然而,你也有可能需要准确的类型转换,或者需要在程序中表明你是知道正在做类型转换的。完成这一任务的方法被称为指派(cast),其步骤是在某个量的前面放置用圆括号括起来的被希望转换成的类型名。圆括号和类型呈一起构成了指派运算符(cast operator)。指派运算符的一般形式如下:
(type)
用实际所需的类型(例如 long)来代替 type 。
考虑下面两个代码行,其中 mice 是一个 int 变量。第二包含两个向 int 类型的指派。
mice = 1.6 + 1.7;
mice =(int)1.6 + (int)1.7;
第一个例子使用了自动转换。首先,1.6 和 1.7 相加得到 3.3 。然后这个数通过截尾被转换成整数 3 来匹配 int 类型变量。
第二个例子中,在相加前,1.6被转换成一个整数(1),1.7也是如此。所以 mice 被赋值为 1+1,即 2 。 这两个形式上没有哪个比另一个更准确,只有通过考虑具体编程问题的上下文才能判断哪一个更意义。
通常,你不应该混合使用类型(这就是为什么一些语言不允许这样做),但是偶尔它也是有用的。C 的原则是避免给你设置障碍,但由你承担起不滥用自由的责任。
PS: 总结 : C 中的运算
表 5.4 中列出的是到目前为止我们已经讨论过的运算符:
----------------------------------------------------------------------------------------
赋值运算符 说明
----------------------------------------------------------------------------------------
= 将它右边的值赋给它左边的变量
----------------------------------------------------------------------------------------
算术运算符
----------------------------------------------------------------------------------------
+ 将它右边的值和它左边的值相加
---------------------------------------------------------------------------------------
- 从它左边的值里减掉它右边的值
---------------------------------------------------------------------------------------
- 作为一元运算符,改变它右边值的符号
---------------------------------------------------------------------------------------
* 用它左边的值乘以它右边的值
---------------------------------------------------------------------------------------
/ 用它右边的值去除它左边的值。如果两个操作数都是整数,那么结果被截尾
---------------------------------------------------------------------------------------
% 当它左边的值被它右边的值除时,得到的余数(只对整数)
--------------------------------------------------------------------------------------
++ 对它右边的值加 1 (前缀模式),或者对它左边的值加 1 (后缀模式)
---------------------------------------------------------------------------------------
-- 与 ++ 类似,只不过是 减 1
---------------------------------------------------------------------------------------
其他运算符
----------------------------------------------------------------------------------------
sizeof 给出它右边的操作数的字节大小。操作数可以是在圆括号里的一个类型说明符,例如
sizeof(float);或者是一个具体的变量,数组等的名字,例如 sizeof foo
----------------------------------------------------------------------------------------
(type) 作为指派运算符,它将跟在它后面的值转换成由圆括号中的关键字所指定的类型。
例如,(float)9 将整数 9 转换成浮点数 9.0
----------------------------------------------------------------------------------------
5.6 带有参数的函数
现在你已经很熟悉使用函数的参数了。要掌握函数的下一步是学习如何编写自己的使用参数的函数。现在我们演示一下技巧(此时,你可能需要复习第 2 章“C 语言概述”结尾处的 butler()函数例子,它说明了如何编写不使用参数的函数)。程序清单 5.15 包含一个 pound(),它打印指定数目的英镑符号(#)。这个例子也将说明一些有关类型转换的应用。
程序清单 5.15 pound.c 程序
----------------------------------------------------------------------
/* pound.c -- 定义带有一个参数的函数 */
#include
void pound (int n); // ANSI 风格的原型
int main (void)
{
int times = 5;
char ch = '!'; // ASCII 码值为 33
float f = 6.0;
pound (times); // int 参数
pound (ch); // char 参数自动转换为 int 类型
pound ((int)f); // 指派运算符把 f 强制转换为 int 类型
getchar();
return 0;
}
void pound (int n) // ANSI 风格的函数头
{
while ( n-- > 0)
printf ("#");
printf("\n");
}
运行该程序产生下列输出:
#####
#################################
######
首先,我们研究一下函数头部分:
void pound (int n)
如果函数不接受参数,函数头里的圆括号将包含关键字 void 。因为此函数接受一个 int 类型的参数,所以圆括号里包含一个名字为 n 的 int 类型变量的声明。你可以使用符合 C 的命名规则的任何名字。
声明一个参数就创建了一个被称为形式参数(formal argument)或形式参量(formal parameter)的变量。在本例中,形式参数是叫做 n 的 int 类型变量。像 pound(10)这样的函数调用会把 10 赋给 n 。在本程序中,函数调用 pound(times)把 times 的值(5)赋给 n。我们称函数调用传递一个值,这个值被称为实际参数(actual argument)或者实际参量(actual parameter);所以函数 pound(10)把实际参数 10 传递给函数,然后函数把 10 赋给形式参量(变量 n)。也就是说,main()中的变量 times 的值复制给 pound()中的新变量 n 。
PS : 参数 (argument) 与 参量 (parameter)
尽管术语参数和参量可以互换地使用,但 C99 文档已经规定:对实际参数或者实际参量使用术语参数,对形式参量或者形式参数使用术语参量。遵循这个约定, 我们可以说参量是变量,而参数是由函数调用提供的值,并且将它赋给相对应的参量。
函数中的变量名字是局部的。这意味着在一个函数里定义的名字不会与其他地方相同的名字发生冲突。如果你在 pound()里使用 times 代替 n,将产生一个与 main()里的 times 不同的变量。也就是说,你有两个同名的变量,但是程序可以分清楚它们。
现在我们看看函数调用。每一个函数调用是 pound(times),正如我们所提到的,该函数调用导致 times 的值 5 被赋给 n 。这导致函数打印 5 个英镑符号和一个换行符。第二个函数调用是 pound(ch)。这里 ch 是 char 类型。它被初始化为 !字符,在 ASCII 系统里它意味着 ch 的数值为 33。自动提升机制把 char 类型提升到 int 类型,(在此系统中)它把存储在 1 个字节的 33 转换为存储在 4 个字节中的 33 ,所以值 33 现在以正确的形式被用作函数的参数。最后的调用 pound ((int)f)使用类型指派来将 float 类型的变量转变成这个参数的正确类型。
假设你漏掉了类型指派,如果使用现代 C,程序将为你自动完成类型指派。这是因为在文件头部声明了该函数的 ANSI 原型:
void pound (int n); // ANSI 风格的原型
原型(prototype)是一个函数声明,它描述了函数的返回值和它的参数。这个函数原型说明了关于 pound()函数的两件事情:
· 函数没有返回值。
· 函数接受一个 int 类型的参数。
因为编译器在 main()使用 pound()之前看到了这个原型,所以编译器知道 pound()应该有什么类型的参数,并且在需要使实际参数的类型与原型保持一致时,编译器会插入一个类型指派。例如,函数调用 pound(3.859)将被转换为 pound(3)。
5.7 一个示例程序
程序清单 5.16 列出一个举例说明本章的几个概念的程序,(对于一小部分特定的人)这个程序会派上一定用场。它看起来有些长,但是所有的计算都在接近末尾的 6 行里被执行。程序的大部分用于计算机和用户之间传递信息。我们已经试着使用大量的注释来使程序的意义清晰明白。请通读此程序,在你读完后我们将给出几点说明。
程序清单 5.16 running.c 程序
--------------------------------------------------------------
// running.c -- 一个对于长跑运动员有用的程序
#include
const int S_PER_M = 60; //每分钟的秒数
const int S_PER_H = 3600; //每小时的秒数
const double M_PER_K = 0.62137; // 每公里的英里数
int main (void)
{
double distk,distm; // 分别以公里和英里计数的跑过的距离
double rate; // 以英里/小时为单位的平均速度
int min,sec; // 跑步用时的分钟数和秒数
int time; // 用秒表示的跑步用时
double mtime; // 跑完 1 英里所用的时间,以秒计
int mmin,msec; // 跑完 1 英里所用的时间,以分钟和秒计
printf ("This program converts your time for a metric race \n");
printf ("to a time for running a mile and to your average \n");
printf ("speed in miles per hour.\n");
printf ("Please enter,in kilometers,the distance run \n");
scanf ("%lf",&distk); // %lf 表示读取一个 double 类型的数值
printf ("Next enter the time in minutes and seconds \n");
printf ("Begin by entering the minutes \n");
scanf ("%d",&min);
printf ("now enter the seconds \n");
scanf (" %d",&sec);
// 把时间转换为全部用秒表示
time = S_PER_M * min + sec;
// 把公里转换为英里
distm = M_PER_K * distk;
// 英里/秒 X 秒/小时 =英里/小时
rate = distm / time * S_PER_H;
// 时间/距离 = 跑完每英里的用时
mtime = (double)time / distm;
mmin = (int)mtime / S_PER_M; // 求出分钟数
msec = (int) mtime % S_PER_M; // 求出剩余的秒数
printf ("you ran %1.2f km(%1.2f miles)in %d min,%d sec\n",distk,distm,min,sec);
printf ("%d sec.\nYour average speed eas %1.2f mph\n",msec,rate);
getchar();
getchar();
getchar();
return 0;
}
程序清单 5.16 使用以前在 min_sec 里使用的方法来将最后的时间转变成分钟和少,但是它也使用了类型转换。为什么?因为程序中把秒转换为分钟的那部分需要整形参数,但是把公里转换为英里的那部分涉及浮点数的运算。我们已经使用了指派运算符来使这些转换更加明显。
实际上,可以只使用自动转换来写该程序。我们的确也曾经那样做过,使用 int 类型的 mtime 来强制时间计算转换成整数形式。然而,在我们进行试验的 11 个系统中,那个版本的程序在 1 个系统上不能运行。那个编译器 (一个有点老的版本)没有遵守 C 规则。使用类型指派使你的目的不但对读者很明显,就是对编译器也很明显。
下面是一个输出示例:
This program converts your time for a metric race
to a time for running a mile and to your average
speed in miles per hour.
Please enter,in kilometers,the distance run
10
Next enter the time in minutes and seconds
Begin by entering the minutes
36
now enter the seconds
23
you ran 10.00 km(6.21 miles)in 36 min,23 sec
51 sec.
Your average speed eas 10.25 mph
5.8 关键概念
C 使用运算符来提供多种服务。每个运算符的特性包括所需操作数的数量,优先级和结合性。当两个运算符共享一个操作数时,最后两个特性决定了先应用哪一个运算符。运算符与值结合可以产生表达式,并且 C 的每一个表达式都有一个值。如果你不了解运算符的优先级和结合性,你可能会构造出不合法的或者是与你期望的值不同的表达式;这可不利于你作为一个程序员的声望。
C 允许你写出将不同的数值类型组合在一起的表达式。但是算术运算要求操作数是同一类型的,所以 C 进行自动转换。然而,不依赖于自动转换是一个很好的编程习惯。你应该通过选择变量的正确类型或通过使用类型指派来使类型的选择更明显。那样你就不必担心出现你不希望的自动转换。
5.9 总结
C 有多种运算符,例如在本章中讨论的赋值和算术运算符。总的来说,一个运算符作用于一个或多个操作数来产生一个值。带一个操作数的运算符(例如负号和 sizeof)称为一元运算符。要求两个操作数的运算符(例如加法和乘法运算符)称为二元运算符。
表达式是运算符和操作数的组合。在 C 里,每一个表达式都有一个值,其中包括赋值表达式和比较表达式。运算符优先级的规则帮助决定当对表达式进行求值时,如何组合表达式里的各项。当两个运算符共享一个操作数时,具有较高优先级的运算符先被运算。如果运算符有相同的优先级,结合性(从左到右或都从右到左)决定了哪个运算符先被应用。
语句是对计算机的完整指示,在 C 中通过一个分号来标识。到目前,你已经使用了声明语句,赋值语句,函数调用语句和控制语句。包含在一对花括号里的语句构成了一个复合语句或者代码块。一个特殊的控制语句是 while 循环,只要判断条件保持为真,该循环就重复执行循环体里的语句。
在 C 里,许多类型转换会自动发生。当 char 和 short 类型出现在表达式里或者作为函数的参数时,它们都将被提升为 int 类型。当 float 类型作为一个函数时被提升为 double 类型。在 K&R C (而不是 ANSI C)下,当 float 用于表达式里时也被提升为 double 类型。当把一种类型的值赋给另一种类型的变量时,该值被转换成和那个变量相同的类型。当较大类型的值被转换成较小类型的值(例如,long 变成 short ,或者 double 变成 float )时,它们可能丢失数据,根据本章概括的规则,在混合类型的算术运算的情况下,较小的类型被转换成较大的类型。
当你定义了一个接受一个参数的函数时,你在函数定义里声明了一个变量,或称形式参数。然后在函数调用中传入的值会赋给这个变量,现在就可以在函数里使用该值了。
5.10 复习题
------------------------------------------------------------
1·假定所有的变量都是 int 类型。找出下面每一个变量的值;
a. x = ( 2 + 3 ) * 6;
b. x = ( 12 + 6 ) / 2 * 3
c. y = x = ( 2 + 3 ) / 4;
d. y = 3 + 2 * ( x = 7 / 2);
答
a. x = 30;
b. x = 27; // 要记得运算符的优先级 及运算顺序
c. x = 1, y = 1;
d. x = 3, y = 9;
-------------------------------------------------------------
2. 假定所有的变量都是 int 类型。找出下面每一个变量的值
a. x = (int) 3.8 + 3.3 ;
b. x = (2 + 3) * 10.5;
c. x = 3 / 5 * 22.0;
d. x = 22.0 * 3 / 5;
答
a. x = 6; // 根据运算符的优先级 3.8 是先简化再与 3.3 相加 即 3 + 3.8
b. x = 52;
c. x = 0; // 记住 x 为 int类型 3 /5 后简化为 0 ,再与 22.0 相乘
d. x = 13;
------------------------------------------------------------------
3 . 你怀疑下面的程序里有一些错误。你能找出这些错误吗?
int main (void)
{
int i = 1;
float n;
printf (" Watch out! Here come a bunch of fractions !\n");
while ( i < 30)
{
n = 1 / i;
printf ("%f",n);
}
printf ("That's all,folks \n");
getchar();
return ;
}
答;
第0行: 应该有 #include
第3行; 应该以分号而不是逗号结尾
第6行: while 语句建立了一个无限循环。因为 i 的值保持为 1,所以它总是小于 30.推测一下它的
意思大概是要写成 while (i++ < 30);
第6到8行: 这样的缩排说明我们想要使第 7行和 8行组成一个代码块,但是缺少了花括号会使
while 循环只包括第 7行。应该添加花括号。
第7行; 因为 1 和 i 都是整数,所以当 i 为 1 时除法运算的结果会是 1, 而当 i 为更大的数时
结果为 0 。 使用 n = 1.0 / 1 ; 会使进行除法运算前先转换为浮点数,这样就会产生
非 0 答案。
第8行: 我们在控制语句中漏掉了换行符(\n),这会使数字只要可能就在一行中打印。
第10行: 应该是 return 0;
#include
int main (void)
{
int i = 1;
float n;
printf (" Watch out! Here come a bunch of fractions !\n");
while ( i++ < 30)
{
n = 1.0 / i;
printf ("%f\n",n);
}
printf ("That's all,folks \n");
getchar();
return 0;
}
-------------------------------------------------------------------------------------
4. 这是程序清单 5.9 的另一种设计方法。表面上看,它使用一个 scanf()函数替代了程序清单 5.9中的两个 scanf()。但是该程序不令人满意。和程序清单 5.9 相比,它有什么缺点?
#include
#define S_TO_M 60
int main (void)
{
int sec,min,left;
printf (" This program converts seconds to minutes and ");
printf (" seconds.\n");
printf (" just enter the number of seconds \n");
printf (" Enter 0 to end the program \n");
while ( sec > 0) {
scanf ("%d",&sec);
min = sec / S_TO_M;
left = sec % S_TO_M;
printf ("%d sec is %d min. %d sec.\n",sec,min,left);
printf ("next input?\n");
}
printf ("Bye!\n");
getchar();
getchar();
return 0;
}
答;主要问题在于判断语句(sec 是否大于 0?)和获取 sec 值的 scanf()语句之间的关系。具体地说,第一次进行判断时,程序还没有机会来获得 sec 值,这样就会对碰巧处在那个内存位置中的一个垃圾值进行比较。一个比较笨拙的解决方法是对 sec 进行初始化,比如把它初始化为 1,这样就可以通过第一次判断。但是还有一个问题,当最后输入 0 来停止程序时, 在循环结束之前不会检查 sec, 因而 0 秒的结果也被打印出来。更好的方法是使 scanf()语句在进行 while 判断之前执行。可以通过像下面这样改变程序的读取部分来做到这一点:
scanf ("%d",&sec);
while ( sec > 0) {
min = sec / S_TO_M;
left = sec % S_TO_M;
printf ("%d sec is %d min. %d sec.\n",sec,min,left);
printf ("next input?\n");
scanf ("%d",&sec);
}
第一次获取输入使用循环外部的 scanf(),以后就使用在循环结尾处(也即在循环再次执行之前)的 scanf()语句。这是处理这类问题的一个常用的方法。
----------------------------------------------------------------------------------
5. 下面的程序将打印出什么?
#include
#define FORMAT "%s! C is cool!\n"
int main (void)
{
int num = 10;
printf (FORMAT,FORMAT);
printf ("%d\n",++num);
printf ("%d\n",num++);
printf ("%d\n",num--);
printf ("%d\n",num);
答:
%s! C is cool!
! C is cool!
11
11
12
11
解释一下。第一个 printf()语句与以下语句相同:
printf ("%s! C is cool\n","%s! C is cool! \n");
第二个打印语句首先把 num 的值增加为11,然后打印这个值。第三个打印语句打印 num(值为11),然后 num增加为12,第四个打印语句打印 n 的当前值 即12 ,然后减小为 11 ,第五个打印语句打印 num的当前值为 11 。
------------------------------------------------------------------
6. 下面的程序将打印出什么?
#include
int main (void)
{
char c1,c2;
int diff;
float num;
c1 = 'S';
c2 = 'O';
diff = c1 - c2; // diff = 4
num = diff; // num = 4.00
printf ("%c %c %c, %d %3.2f \n",c1,c2,c1,diff,num);
getchar();
return 0;
}
答:SOS 4 4.00 (表达式 c1 c2 即‘S’和‘O’)在 ASCII码中,= 83 79
-------------------------------------------------------------------------
7. 下面的程序将打印出什么?
#include
#define TEN 10
int main (void)
{
int n = 0;
while ( n++ < TEN)
printf ("%5d",n);
printf ("\n");
getchar();
return 0;
}
答; 1 2 3 4 5 6 7 8 9 10
---------------------------------------------------------------------------
8. 修改上一个程序,让它打印 从 a 到 g 的字母
答:
/* 假定字母是连续编码的,就像 ASCII 中的情况那样 */
#include
int main (void)
{
char c = 'a';
while (c <= 'g')
printf ("%5c",c++);
printf ("\n");
getchar();
return 0;
}
---------------------------------------------------------------------------
9. 如果下面的片段是一个完整程序的一部分,它们将打印出什么/
a
int x = 0;
while (++x < 3)
printf ("%4d",x);
b
int x = 100;
while (x++ < 103)
printf ("%4d\n",x);
printf ("%4d\n",x);
c
char ch = 's';
while (ch < 'w')
{
printf ("%c",ch);
ch++;
}
printf ("%c \n",ch);
答
a: 1 2 // 因为++是前缀,所以 x 先是被递增然后再进行比较
b: 101 // 注意这次 x 先进行比较然后再递增。在这里和例 a 的情况中,x 都是在打印
102 之前被递增。还要注意第二个 printf()语句缩进并不能使它成为 while
103 循环的一部分。因此它只是在 while 循环结束之后被调用一次
104 // printf ("%4d\n",x); 第二个 printf()起的作用。
c: stuvw // 这里,直到第一次调用 printf()之后才对 ch 进行递增。
--------------------------------------------------------------------------
10. 下面的程序将打印什么/
#define MESG "COMPUTER BYTES DOG"
#include
int main (void)
{
int n = 0;
while (n < 5);
printf ("%s\n",MESG);
n++;
printf ("That's all.\n");
getchar();
return 0;
}
答: 无打印结果,偶也不知道那处出错,和答案不一样,但用 BCB 2010 VC++ 2005
输出无结果。
-----------------------------------------------------------------------------
11. 构造完成下面的功能(或者用一个术语来说,有下面的副作用)的语句:
a. 把变量 x 的值增加 10
b. 把变量 x 的值增加 1
c. 将 a 与 b 之和的两倍赋给 c
d. 将 a 与两倍的 b 之和赋给 c
答
a. x = x + 10;
b. x++;
c. c = 2*(a+b)
d. c = a + 2*b
----------------------------------------------------------------------------
12. 构造具有下面功能的语句:
a. 把变量 x 的值减 1
b. 把 n 除以 k 所得的余数赋给 m
c. 用 b 减去 a 的差去除 q,并将结果赋给 p
d. 用 a 与 b 的和除以 c 与 d 的乘积,并将结果赋给 x
答;
a. x--
b. m = n % k
c. p = q / (b - a)
d. x = (a+b)/ (c*d)
注意:除数与被除数
---------------------------------------------
5.11 编程练习
------------------------------------------------------------------------------
1. 编写一个程序。将用分钟表示的时间转换成以小时和分钟表示的时间。使用 #define 或者 const来创建一个代表 60 的符号常量。使用 while 循环来允许用户重复键入值,并且当键入一个小于等于0的时间时终止循环。计数
解:
#include
#define UNIT 60
int main (void)
{
int int_hour;
int int_count;
printf ("请输入分钟数 (注意:<=0 程序将退出)\n");
scanf ("%d",&int_count);
while (int_count > 0)
{
printf("%d = %d 小时 %d 分钟\n",int_count,int_count/UNIT,int_count % UNIT);
scanf ("%d",&int_count);
}
getchar();
return 0;
}
------------------------------------------------------------------------------------
2. 编写一个程序,此程序要求输入一个整数,然后打印出从(包括)输入的值到(包括)比输入的值大于10的所有整数值(也就是说,如果输入为 5,那么输出就从 5 到 15)。要求在各个输出值之间用空格,制表符或换行符分开。
解:
#include
int main (void)
{
int num,num1;
printf(" 请输入一下整数,(非零程序将退出)\n");
scanf ("%d",&num);
while (num > 0)
{
num1 = num + 10;
while (num <= num1)
printf ("%4d",num++);
printf(" \n 请输入一下整数,(非零程序将退出)");
scanf ("%d",&num);
}
getchar();
return 0;
}
注意:比如 用scanf()函数之后的 while循环,最好在while循环的结尾处也用此函数做为循环
否则,循环可能不成功。
-----------------------------------------------------------------------------
3. 编写一个程序,该程序要求用户输入天数,然后将该值转换为周数和天数。例如,此程序将把 18 天转换成 2周 4天。用下面的格式显示结果:
18 days are 2 weeks,4 days 。
使用一个 while 循环让用户重复输入天数,当用户输入一个非正数(如 0 或 -20)时,程序将终止循环。
解:
#include
int main (void)
{
const EOOR = 7 ;
int days,weeks,days1;
printf (" 请输入一个数 (非正数则程序退出)\n");
scanf ("%d",&days);
while (days > 0) // 非正数退出
{
weeks = days / EOOR;
days1 = days % EOOR;
printf (" %d days are %d weeks, %d days\n",days,weeks,days1);
printf (" 请输入一个数 (非正数则程序退出)\n"); // 循环开始
scanf ("%d",&days);
}
getchar();
getchar();
return 0;
}
强调: 还是要循环怎么令之循环的问题
------------------------------------------------------------------------------------------
4. 编写一个程序让用户按厘米输入一个高度值,然后,程序按照厘米和英尺英寸显示这个高度值。允许厘米和英寸的值出现小数部分。程序允许用户继续输入,直到用户输入一个非正的数值。程序运行的示例如下所显:
Enter a height in cedtimeters : 182
182.0cm = 5 feet, 11.7 iches
解:
#include
#define TEMPCM 0.0328083 // 1 厘米 = 0.032808399 英尺
#define TEMPINCH 0.083333 //1英尺 = 0.0833333333 英寸
int main (void)
{
int feet;
float cm,inch;
printf ("Enter a height in cedtimeters : ");
scanf ("%f",&cm);
while (cm > 0)
{
feet = (int)cm * TEMPCM;
inch = (cm * TEMPCM - feet) / TEMPINCH;
printf("%.1f cm = %d feet, %.1f iches\n",cm,feet,inch);
printf ("Enter a height in cedtimeters : ");
scanf ("%f",&cm);
}
printf("bye,your num < 0");
getchar();
return 0;
}
注: 强调的还是循环的部分要注意,要形成循环 等于是把 while 语句前二个提示和输入 也包含在while 循环体内,才能形成有效的循环。
----------------------------------------------------------------------------------
5. 改写用来找到前 20个整数之和的 addemup.c (程序清单5.13)(如果你愿意,可以把 addemup.c程序看成是一个计算如果你第一天得到$1,第二天得到$2,第三天得到$3,以此类推,你在20天里会挣多少钱的程序)。修改该程序,目的是你能交互地告诉程序计算将进行到哪里。也就是说,用一个读入的变量代替 20
解:
#include
int main (void)
{
int day,num,count;
num = 0;
day = 0;
count = 0;
printf("请输入你想知道的天数: ");
scanf ("%d",&count);
while (count > 0) // 遇 0 退出
{
while (day++ < count)
num = num + day; // 这个语句起了累积计数的效果
printf("%d 天,共收入$%d \n",count,num); // 开始循环
printf("请输入你想知道的天数: ");
num = 0; // 将全部变量重置为 0
day = 0;
count = 0;
scanf ("%d",&count);
}
printf ("对不起,你没有输入天数");
getchar();
getchar();
return 0;
}
------------------------------------------------------------------------------------------
6. 现在修改编程练习 5 中的程序,使它能够计算整数平方的和(如果你喜欢,可以这样认为:如果你第一天得到 $1,第二天得到$4,第三天得到$9,以此类推你将得到多少钱)。C 没有平方函数,但是你可以利用 n 的平方 是 n*n 的事实。
#include
int main (void)
{
int day,num,count,num1;
num = 0;
day = 0;
count = 0;
printf("请输入你想知道的天数: ");
scanf ("%d",&count);
while (count > 0) // 遇 0 退出
{
while (day++ < count)
{
num1 = day * day;
num = num1 + num;
} // 这个语句起了累积计数的效果
printf("%d 天,共收入$%d \n",count,num); // 开始循环
printf("请输入你想知道的天数: ");
num = 0; // 将全部变量重置为 0
day = 0;
count = 0;
num1 = 0;
scanf ("%d",&count);
}
printf ("对不起,你没有输入天数");
getchar();
getchar();
return 0;
}
------------------------------------------------------------------------------------------
7. 编写一个程序,该程序要求输入一个 float 型数并打印该数的立方值。使你自己设计的函数来计算该值的立方并且将它的立方出来。 main()程序把输入的值传递给该函数。
解:
#include
int func (float n);
int main (void)
{
float num;
printf ("请输入一个正数,整数将显示它的立方值");
scanf ("%f",&num);
while (num >= 0)
{
func(num);
printf ("请输入一个正数,整数将显示它的立方值");
scanf ("%f",&num);
}
printf ("\n 对不起,你输入的不是一个正数");
getchar();
getchar();
return 0;
}
int func (float n)
{
printf ("\n %.2f X %.2f X %.2f = %.2f",n,n,n*n,n*n*n);
}
---------------------------------------------------------------------------------
8. 编写一个程序,该程序要求用户输入一个华氏温度,程序以 double 类型读入温度值,并将它作为一个参数传递给用户提供的函数 Temperatures()。该函数将计算相应摄氏温度和绝对温度,并以小数点右边有两位数字的精度显示这三种温度。它应该用每个值所代表的温度刻度来标识这 3 个值。下面是将华氏温度转换成摄氏温度的方程
Celsius = 1.8 * Fahrenheit + 32.0 公式错误 应为 Celsius = (5/9) * (Fahrenheit - 32)
通常用在科学上的绝对温度的刻度是0代表绝对零,是可能温度的下界。下面是将摄氏温度转换为绝对温度的方程:
Delvin =Celsius +273.16
Temperatures()函数使用const来创建代表该转换里的3个常量的符号。mian()函数将使用一个循环来允许用户重复地输入温度,当用户输入q或其他非数字值时,循环结束
解:
#include
const double A = 0.555556;
const double B = 32.0;
const double DELVIN = 273.16;
int Temperatures (float temp);
int main (void)
{
double fah;
printf ("请输入数字,按‘Q’或其他字母程序将退出 :");
scanf("lf",&fah);
while (scanf("%lf",&fah) == 1)
{
Temperatures(fah);
printf ("请输入数字,按‘Q’或其他字母程序将退出 :");
scanf("lf",&fah);
}
printf ("程序将退出");
getchar();
getchar();
return 0;
}
int Temperatures (float temp)
{
double celsius,delvin;
celsius = (temp -32)* A;
delvin = celsius + DELVIN;
printf (" 华氏 摄氏 绝对零度 \n");
printf (" %.2f %.2f %.2f \n",temp,celsius,delvin);
}
注意:1. == 为 等于 while (scanf("%lf",&fah) == 1) 该语句就判断了 非数字值退出循环,
!= 为 不等于 while (scanf("%lf",&fah) != 0) 一样的效果
2. double 双精度 只能显示小数点后 6位 例如 0.555555 如果计算 (5/9)将无法计算出来
------------------------------------------------------------------------------------------