灯火互联
管理员
管理员
  • 注册日期2011-07-27
  • 发帖数41778
  • QQ
  • 火币41290枚
  • 粉丝1086
  • 关注100
  • 终身成就奖
  • 最爱沙发
  • 忠实会员
  • 灌水天才奖
  • 贴图大师奖
  • 原创先锋奖
  • 特殊贡献奖
  • 宣传大使奖
  • 优秀斑竹奖
  • 社区明星
阅读:5799回复:0

C语言语句及流程控制

楼主#
更多 发布于:2014-03-24 11:11
本章重点
u 语句和语句块
u 变量的作用域
u 顺序结构
u 判断结构
u 循环结构
从本章开始,读者将可以真真正正地写一个完整的C程序了!我们将在本章中介绍C程序的三种最基本的结构:顺序结构,选择结构和循环结构。读者们很快就会发现,掌握了这些最基本的程序结构之后,我们就可以用C语言写出很多很多有意思的程序。
语句和语句块
如果把一个C程序比喻成一栋高楼,那么一行行的语句就是垒成高楼的砖块。程序员的工作就是把这些相对简单的砖块组织在一起,建造出风格不同,功能不同的建筑。
简单语句
粗略地说,一条简单语句就是一个最基本的执行单元。在前面几章中我们已经接触了很多简单语句。比如:Hello world中的printf:
printf("Hello world!n");
和紧接着的返回语句:
return 0;
变量的定义和声明:
int i = 10;
简单的运算:
sum = 1.7f + 2.5f;
这是一条赋值语句,它将右边数学表达式的结果赋值给左边的变量。在书写简单语句时,不要忘记在结尾加上分号。
语句块
语句块是用花括号括起来的一行或多行语句。比如:
{
float sum = 0.0f;
sum += 3.5f;
printf("%f", sum);
}
这是一个语句块,它包含了三条语句:第一条语句定义了一个变量sum,并将sum的值初始化为0;第二条语句将sum的值自增了3.5;第三条语句将sum的值输出到控制台上。
将语句组织成语句块一般并不会影响语句的执行效果,不过合理的使用语句块可以让程序在逻辑上显得更加清晰。我们在本章随后将要看到的判断结构和循环结构中都包含了语句块,这些语句块能让我们更清楚地识别这些结构。
变量的作用域
我们在第三章中简单地介绍过变量。现在我们已经知道,每一个变量需要有自己的名字和类型。如果变量被初始化,那么它还会有一个初值。在这一节中,我们将介绍变量的作用域。读者可以把变量的作用域理解为这个变量在程序的哪些地方是可以被有效访问的:在这个有效范围之内,我们可以正确操作这个变量,如给变量赋值,对变量进行计算,输入输出等;在这个有效范围之外,我们不能使用这个变量。
变量包括局部变量和全局变量。局部变量定义在一个函数内,只在这个函数内使用,比如我们之前的所有例程中,所有的变量都是定义在main函数内部的,也只能在main函数里使用;全局变量定义在函数外,可以被程序的所有函数使用。在这一节中,我们重点向大家介绍局部变量的作用域。
局部变量的声明定义位置规则
在C中,如果你想要使用一个变量,你必须要事先声明它。在声明变量之前使用它是不合法的,比如下面的例子:
#include
 
int main()
{
sum = 3;
int sum;
return 0;
}
在main函数中我们声明了一个变量sum,但是在声明它之前我们对sum进行了赋值。问题就来了:在对sum进行赋值时,sum还没有定义,于是编译器并不知道sum究竟是个什么东西。如果你在VS中尝试编译,编译器会提示你sum未定义。
 
局部变量的作用域规则
我们之前说过局部变量是定义在函数内的,上一节中的例子又告诉了我们变量要在定义之后才能使用。把两者结合起来,我们就可以得到局部变量的作用域:
一般地,局部变量的作用域从定义开始,到函数的右括号结束。
以我们最熟悉的main函数为例,上面的作用域定义相当于告诉我们:在main函数中定义的局部变量,从定义的地方开始,到
嵌套语句块的同名变量作用域规则
如果程序中包含了语句块,语句块可能会影响变量的作用域。请看下面的例子:
:动手体验:在语句块内使用变量
#include
 
int main()
{
int sum = 3;
{
int sum = 6;
printf("%dn", sum);
}
printf("%dn", sum);
return 0;
}
我们首先在main函数中定义了一个变量sum,并将它赋值为3。随后我们定义了一个语句块,在语句块内重新定义了一个变量sum,并初始化为6,同时在语句块内输出sum的值。猜猜看这时候输出的sum是多少?
跳出了语句块之后,我们再次输出sum的值,猜猜看,这次输出的sum又是多少?
如果在语句块中定义了变量,那么这个变量的作用域从定义开始,一直延伸到语句块结束。如果它和语句块外部的某个变量重名了,在语句块内访问到的将是语句块内定义的变量。换句话说,外部的重名变量在语句块内失效了。
在刚刚的例子中,进入main函数首先定义的变量(我们称它为sum1)
int sum = 3;
它的作用域是从定义开始,一直到main函数结束。而在语句块内定义的sum(我们称它为sum2)
int sum = 6;
它的作用域是从定义开始,一直到语句块结束。而且,由于它和语句块外部的sum重名了,在语句块内的printf能看到的和能使用的都将是sum2。
执行完了第一个printf之后,我们跳出了语句块,也就离开了sum2的作用域。但是,由于我们还在main函数内,因此这个时候我们还在sum1的作用域里,所以,第二个printf能够访问的sum是sum1。最后我们的输出结果如下:
6
3
 
 
最常见的语句执行顺序——顺序结构
介绍完了语句和变量的作用域之后,我们就可以开始编写一些真正的程序了!在本节中我们会向大家介绍顺序结构,这是最常见的语句执行顺序。让我们从一个例子开始:
想象你是一个刚上路的出租车司机,这时来了一位乘客:“请把我送到机场。”
很不幸的是,作为一个菜鸟,你自己根本不认识去机场的路,于是你很无奈地表示自己恐怕不能送他去机场了。“哦,没关系。”乘客说,“你出了城区,沿着高速公路一直向前开就可以了,到了出口我会告诉你的。”
C语言中的顺序结构和上面的这个例子很相似。在顺序结构中,程序也只有唯一的一条“路”可以走,那就是从前向后一条一条地执行程序中的语句。我们其实早就看过这样的程序:
#include
 
int main()
{
printf("Hello world!n");
return 0;
}
在这个例子中,程序从main函数开始后,按照从上往下的顺序一步一步地执行printf和return两行语句。执行完成后,整个程序也就结束了。
读者可能会疑惑:所有的程序不都应该是这样吗?并不完全是这样。正如同在路上开车会遇到岔路口一样,在程序执行过程中也会遇到一些“岔路口”。在这些岔路口处我们需要做出判断,决定走岔路口中的哪一条路,有的时候我们会跳过一些代码,这就是后面要介绍的判断结构。还有的时候,我们要在一小段代码中来回跑,这时候我们的程序就像是北京地铁二号线的环线一样,在一小段程序中来回绕圈,直到找到一个合适的地方再出去,这就是循环结构。我们将在后面两节更加详细地介绍这两种结构。在此之前,还是让我们首先从最简单的顺序结构开始。
基本输入输出
让我们首先从基本的输入输出语句开始。严格地说,它们和顺序结构没有什么关系,但是学会了基本输入输出语句可以让我们快速地开始编写有意思的程序,因此我们把基本输入输出语句放在这里介绍,以方便读者们快速上手。
我们首先从基本的输出语句printf开始。printf是一个很神奇的东西,它可以向控制台输出我们想要显示的内容,比如最简单的Hello world:
printf("Hello, world!");
这是我们要介绍的printf的第一种用法:直接输出一个字符串。不要忘记用一对英文双引号把要输出的字符串括起来。如果你还记得字符类型中的n,你也可以试试在Hello world后面加上一个n:
printf("Hello, world!n");
仔细看看,输出的结果有什么变化没有?记住:字符串无非就多个连接在一起的字符类型数据,printf会将每一个字符忠实地显示在屏幕上。
:动手体验:用printf输出字符串
新建一个工程,或者找到之前你的Hello world程序,尝试下面的printf语句:
printf("我是反斜杠\");
以及:
printf("如何打印双引号 "");
在运行程序之前,先猜猜看输出的结果会是什么样的?
当然,如果你新建了一个工程,不要忘了加上必要的include和main函数!
除了输出一个字符串,printf还有更加灵活的用法:输出格式控制字符串。假设我们有一个float类型的变量a,我们将它初始化为0:
float a = 0;
如果我想在屏幕上输出a的值,应该怎么办呢?按照我们此前的方法,我们可以这样:
printf("0");
好吧,这算是一种解决方法。可是,如果我们在写程序的时候不知道a的值,却希望在程序运行到这里的查看a的大小,应该怎么办呢?
举个例子:我正在写一个能将千克转成磅的小程序,由于我的同事fish恰好写过这样的一小段代码。作为一个懒惰的程序员,我懒得去google这两个单位的转换公式,而是将fish的那几行关键代码复制了过来:
int weight = 62.0f;//以千克为单位
//从这里开始是fish的代码,负责把weight的值转成以磅为单位的重量
blablabla......
blablabla......
blablabla......
//计算完成了!这个时刻weight里面保存以磅为单位的重量,我想在屏幕上显示weight的值:
printf(???);
这里就是我们要介绍的printf的另一种用法:格式化输出字符串。对于上面这个例子,我们可以用这样的printf语句来完成:
printf("weight的值是 %f.", weight);
在这里我们的字符串当中出现了一个新的符号%f,它表示在字符串的这个地方我们要输出一个浮点数,浮点数的值在后面的weight变量里面。在这里,%f像是一个占位符,告诉printf函数:我在这里要输出一个浮点数了,请到后面去查找这个浮点数的值,然后用那个值来替换我!
如果你还是不太清楚%f是什么意思,我们来看一个生活中的例子:假设我正在组织大学同学的聚会,我统计了一张表,按照当年的学号顺序,挨个打电话问大家现在的家庭住址:
学号
住址
1
A1市B1小区C1室
2
A2市B2街C2号
3
 
4
A3市B3镇C3乡
5
 
6
 
很遗憾,我发现我暂时打不通3号,5号和6号同学的手机,于是我给他们发了短信:“我在统计大家的家庭住址,打不通你的电话,看到短信后请回复。”现在我的这张表就变成了这个样子:
学号
住址
1
A1市B1小区C1室
2
A2市B2街C2号
3
等回复
4
A3市B3镇C3乡
5
等回复
6
等回复
很快,3号同学回复了一个“A4市B4小区C4栋”,我找到表格中3号的地址那一栏,把原来的“等回复”删了,换成了他的回复。
类比到printf的例子当中,这里的“等回复”就像是printf当中的%f一样,我们并不是要在这里显示“%f”或者是“等回复”这三个字,而是要用别的内容来替换它们。在printf的例子中,我们是把随后float类型的变量weight的值放在%f的位置上输出;在同学会的例子中,我们是把同学回复的短信放在“等回复”的位置上。
类似地,我们用%d来表示输出一个整数,%c表示输出一个字符,%x表示用十六进制形式输出一个整数,等等。
表 51常用printf格式字符
常用格式字符
含义
%c
输出一个字符
%d
以十进制输出一个整型
%x
以十六进制输出一个整型
%f
输出一个浮点数
%s
输出一个字符串
需要指出的是:printf不限于每次只能输出一个格式化字符。这就好像在同学会的例子中,我们的表格中可能有不止一个地方是“等回复”一样。当我们想要同时输出多个格式化字符时,我们要按照顺序给出这些格式字符的值。比如下面的这个例子:
int i = 15;
float f = 15.0f;
char c = 'c';
printf("i = %d, f = %f, c = %c.", i, f, c);
显示结果如下:
i = 15, f = 15.000000, c = c.
请读者注意printf后面几个变量的顺序,他们和格式控制字符的顺序(整型,浮点,字符)的顺序必须是一致的。
解释完了printf,用于格式化输入的scanf就很好理解了。scanf函数用来从控制台读入用户的输入信息。假设我们希望用户输入自己的身高:
printf("请输入你的身高n");
int height;
scanf("%d", &height);
printf("你的身高是%dcm.n", height);
这个程序运行起来之后效果是这样的:
请输入你的身高
172
你的身高是172cm.
我们看到这里出现了之前大家从未见过的scanf语句:
scanf("%d", &height);
scanf用来从控制台读入用户的输入。和printf一样,%d在这里表示从控制台上要读入的是一个整数。后面的height表示将读入的整数保存在height变量中。和printf稍有不同的是,我们发现height前面还有一个新符号&,这个符号,加上后面跟着的height,表示我们要取得height变量在内存中的地址,这是printf函数和scanf函数不太一样的地方。
对于新接触计算机的读者来说,这里的&往往会让人无比困惑。没有关系,我们将在之后的函数和指针等章节中为大家详细介绍&的含义和用法。现在读者们只需要知道,当我们想要调用scanf的时候,我们需要告诉scanf一个变量的地址,让scanf把读入的输入保存在这个地址当中。
为了方便大家理解和记忆printf和scanf的用法,我们在这里举一个不太准确的例子来类比一下:
在计算机市有一条内存大街,内存大街上住着很多很多变量居民,每一个变量都有自己的门牌号(变量在内存中的地址)。计算机市还有一家控制台邮局,这个邮局负责收发变量居民们的信件。
变量weight住在内存大街的196号。有一天,它给控制台邮局写了一封信,信的内容就是它自己的名字62(变量的值)。它把信封好之后,写上
控制台邮局 收,
内存大街196号 寄
交给了邮递员printf。不幸的是,printf送信送到一半的时候下起大雨,信封上的寄件人地址被淋湿看不清了。可是对于printf来说,寄件人的地址重要吗?不重要!printf只要把信件本身送到控制台邮局就可以了!从printf的观点来看,这两段代码没有什么区别:
代码一:
int weight = 62;
printf("我的体重是%d.", weight);
代码二:
printf("我的体重是%d.", 62);
对于printf来说,传进去一个变量weight后,不用关心这个变量叫什么名字,也不关心变量住在哪里,只关心这个变量里面的值是多少,把变量值取出来,打印在控制台上就可以了!
然而,有一天控制台邮局收到了外地寄来的一封信:
XX用户 寄,
内存大街224号, height 收
这个时候,控制台邮局的另一名邮递员scanf负责把这封信交给住在内存大街224号的变量height。printf每次送信可以不关心内存大街上那些寄信人的地址,但是scanf不能!如果不知道height的具体住址,scanf就不能把这封信准确地投递给height家里。对于scanf来说,变量名height不重要,height里面存的值也不重要,scanf只想知道的是自己要把寄来的信(用户从控制台的输入)投递到哪一家住户那里(保存在内存中的什么位置)。所以在使用scanf的时候,我们必须要用&来取出变量的地址,并把这个地址告诉scanf。
以上是对printf和scanf的一个粗糙类比,仅仅用于帮助大家理解。读者可以耐心地等到学习后面的内容时再回想printf和scanf的调用过程,现在我们只要知道怎么使用它们就可以了!
scanf也可以一次读取多个变量:
int i;
float f;
char c;
scanf("%d %f %c", &i, &f, &c);
printf("i = %d, f = %f, c = %cn", i, f, c);
在我的电脑上,运行结果如下:
17 32.5 h
i = 17, f = 32.500000, c = h
和printf一样,scanf当中格式控制字符的顺序和随后变量的顺序也必须一致。
顺序结构实例
怪兽大学是一所致力于培养惊吓专员的大学。惊吓专员的目的是恐吓人类婴儿,利用婴儿的尖叫声充满电量瓶来获取惊吓能量。小怪兽麦克华斯基明天就要参加期末考试了。它要潜入模拟房间去恐吓床上的机器婴儿。机器婴儿受到惊吓后发出的分贝声越高,华斯基的考试分数就越高。在考试前,主考官郝刻薄院长公布了今年的量化评分标准:
考生得分 = 婴儿分贝 / 40 + 2.5
其中,婴儿分贝的取值范围为0到100间(含0和100)的整数。
郝刻薄院长希望今年的评分能够用计算机在现场完成:只要输入一个婴儿的分贝数,计算机就可以返回该名考生的得分。然而怪兽大学没有计算机系,因此郝刻薄院长希望你能够写一个小程序来帮助她。
程序输入:一个0到100之间(包含0和100)的整数
程序输出:考生的得分,用浮点数表示。
程序示例:
程序输入:100
程序输出:5.000000
(本题背景来自皮克斯工作室和迪斯尼合作的3D喜剧动画片Monster University)
 
分析:
这个程序的结构非常清晰。我们可以把整个程序分解成三个部分:
从控制台读入一个整数;
计算考生得分;
输出考生的得分;
按照这个思路,我们首先写出整体的框架。首先,我们定义两个变量voice和score:
#include
 
int main()
{
int voice;
float score;
//从控制台读入一个整数
//计算考生得分
//输出考生的得分
return 0;
}
我们接下来一步一步来完成整个程序。首先从第一步开始:根据前面的知识,读入一个整数可以用scanf来完成:
scanf("%d", &voice);
接下来,我们来计算考生的得分:
score = voice / 40 + 2.5;
最后,我们把score输出到控制台上:
printf("%f", score);
我们的程序完成了!
#include
 
int main()
{
int voice;
float score;
//从控制台读入一个整数
scanf("%d", &voice);
//计算考生得分
score = voice / 40 + 2.5;
//输出考生的得分
printf("%f", score);
return 0;
}
来运行一下:
100
4.500000Press any key to continue . . .
这个结果不太对啊,输入100的时候输出应该是5分,怎么会是4.5呢?我们来一步一步看:
printf会有错吗?不太可能,我们写的是%f,变量名也没有写错。这说明到了这一步score的值确实是4.5;
那这个4.5是怎么计算出来的呢?回头看看程序:
score = voice / 40 + 2.5;
score如果是4.5的话,那voice / 40的计算结果就是2,难道voice是80?可是我们输入的确实是100。是scanf把输入的100改成了80吗?也不太可能,我们用了%d,也写了&voice,scanf不太可能把值改掉。
那就只剩下最后一种可能了:100 / 40 = 2?哦,原来我们在这里犯了错误:我们使用的100是一个int类型的整数,除数40也是一个整数,当两个整数相除时,结果会自动取整(请回忆4.2.4节)!难怪我们算出来的2.5被硬生生地改成了2!
明白了错误在哪里,程序就很好改了:我们只需要把100强制转换成浮点数,浮点数除以整数的时候会按照浮点数除法来运算,这样我们就可以得到正确的结果了:
#include
 
int main()
{
int voice;
float score;
//从控制台读入一个整数
scanf("%d", &voice);
//计算考生得分
score = (float)voice / 40 + 2.5;
//输出考生的得分
printf("%f", score);
return 0;
}
运行一下试试:
100
5.000000
OK!这次结果就对了。
尽管这是一个非常简单的例子,它还是可以给我们带来很多启示的:首先,如果拿到了一个程序不知从何下手,不妨把它分解成几个部分,一步一步来完成;第二,程序出错了不要紧,要逐步缩小出错的范围,找到错误,改正它!
本书附录中介绍了程序调试的技巧,读者现在就可以对照附录开始学习如何进行程序的调试,并且从写自己的第一个程序开始实践程序调试的方法。
 
:动手体验:
盖住上面的源代码,把这个例题重新写一遍,你能够一次通过吗?如果不能的话,是哪里出错了?利用附录中的调试技巧找出错误并改正。
if判断结构
回到那个出租车司机的例子。当我们在路上遇到三岔路口或是十字路口时,我们需要从多种可能中选择一条路前进。在程序中也有这样的“岔路口”。当遇到这些岔路口时,程序的执行顺序就不再是简单地一条一条从上往下顺序执行,而是会按照一定的规则来决定接下来执行哪一条语句。这就是本节的if判断结构和下一节要介绍的switch判断结构。它们的基本思路是:首先判断一个条件的真假,随后根据判断的结果选择接下来执行哪一部分语句。
基本if结构
我们首先来介绍if-else语句。让我们从最简单的if语句开始:
if (条件判断)
{
...
}
if语句应该这样翻译:如果条件判断的值为真,那么就进入花括号内的语句块去执行里面的程序。当然,如果条件判断的值为假,那就跳过if的这一部分,继续执行后面的代码。我们还是来看一个简单的例子:
int number;
scanf("%d", &number);
if (number % 2 == 0)
{
printf("%d是个偶数n", number);
}
让我们来一句一句地解释这一段代码在干什么:
int number;
这没有什么问题,我们定义了一个int类型的变量number;
scanf("%d", &number);
这个也不难,这是我们上一节学习的scanf语句,用户从控制台上输入一个整数,我们把它保存在number中;
if (number % 2 == 0)
这就有点意思了。回忆一下%和==都是什么意思?%表示的是整数间的取模运算,而==用来判断左右两边的值是否相等。我们把这句话翻译成汉语那就是:number这个变量的值除以2的余数等于0。如果这句话为真了,我们就进入if语句下面的花括号:
{
printf("%d是个偶数n", number);
}
我们在这里打印了一行字:number代表的这个数是一个偶数。现在我们把if语句的这一块连在一起理解,这段代码的功能就是:如果number除以2的余数是0,我们就在屏幕上输出:number是个偶数。
M脚下留心:==与=
初学者最容易犯的错误是忘记区分==和=两个符号,尤其是在if语句当中,一不小心就会写出下面这样的语句:
if (number % 2 = 0)
{
...
}
再次强调,==和=的含义是完全不一样的!==是判断左右两边的值是否相等,而=是将右边的值赋给左边。上面的这种错误还可以在编译时检查出来,因为number % 2 是不能被赋值的。更隐蔽的错误是这种:
int value = number % 2; //value保存了number除以2的余数
if (value = 0)
{
...
}
你会发现,无论number是奇数还是偶数,你都进入不了if里面去执行语句。为什么呢?因为value=0的真值是value最终的值,而value的值已经被赋为0了,所以,它的值永远都是假。此外,在条件判断时,非零即为真。所以,不仅仅if(value=1)是一个永真的判断,只要value被赋予了一个非零的值,比如if(value=2)或者if(value=10)都是永真的判断。如果你的程序中条件判断的执行总是和你的预期不一致,请首先检查一下自己是不是犯了上述错误。
if…else…结构
解释完了if之后,我们接下来介绍else:
if (条件判断)
{
...
}
else
{
...
}
不要被else吓到,它只是补充了一下if当中条件判断为假时执行的代码:如果条件判断为真,我们就进入if;如果条件判断为假,我们就进入else。无论真假,我们总要执行,且仅执行if和else当中的一个。
还是回到奇偶数的例子上来。我们将刚刚的例子稍加改动:
int number;
scanf("%d", &number);
if (number % 2 == 0)
{
printf("%d是个偶数n", number);
}
else
{
printf("%d是个奇数n", number);
}
通过刚刚的介绍,你应该很容易理解这段代码在干什么:如果我们输入的是一个偶数,那么条件判断为真,我们进入if语句块,在屏幕上输出这个数是偶数;否则,条件判断为假,我们就进入else,并且在屏幕上输出这个数是个奇数。
多学一招:
如果if或是else里只有一条语句,那么外面的花括号可以省略。比如刚刚的例子也可以写成:
int number;
scanf("%d", &number);
if (number % 2 == 0)
printf("%d是个偶数n", number);
else
printf("%d是个奇数n", number);
当然你也可以一直把一对{}带着,这取决于你喜欢哪种代码风格。
if…else if…else结构
If加上else只能帮我们处理二选一的情况。然而,正如同在城市里开车还会遇到三岔路口和十字路口一样,有的时候我们还需要能够处理三选一,四选一乃至N选一的情况。比如在美剧《生活大爆炸》中,男主角谢耳朵有一张严格的日程表,精确到每星期几应该吃什么:
表 52谢耳朵的菜单
周一
周二
周三
周四
周五
周六
周日
燕麦粥
汉堡
奶油土豆汤
披萨
法式面包
中餐

谢耳朵周日吃啥是个谜,笔者没有考证出来。我们就假定谢耳朵周日自己做饭吧。
现在,如果你要给谢耳朵写一个日程管理程序,输入星期几,输出今天要吃什么。这是一个N选一的选择结构。由于N选一可以被拆成很多很多个二选一,利用手头已知的if和else语句,机智的读者可以写出如下的嵌套结构:
if (今天是星期一)
{
我们吃燕麦粥;
}
else
{
if (今天是星期二)
{
我们吃汉堡;
}
else
{
if (今天是星期三)
{
我们吃奶油土豆汤;
}
else
...
}
}
这个程序当然是对的,但是这样的程序既难读又难写。幸好C语言中除了if和else之外还有专门为N选一设计的else if结构,它可以让我们不必写上面这种嵌套的if程序:
if (今天是星期一)
{
我们吃燕麦粥;
}
else if (今天是星期二)
{
我们吃汉堡;
}
else if (今天是星期三)
{
我们吃奶油土豆汤;
}
else if (今天是星期四)
{
我们吃披萨;
}
else if (今天是星期五)
{
我们吃法式面包;
}
else if (今天是星期六)
{
我们吃中餐;
}
else
{
//今天是星期天
谢耳朵要自己做饭!
}
结合这个例子,else if干了什么应该很好懂了。首先,判断if的条件是否成立,如果成立,意味着今天是星期一,进入if;如果不成立,我们就去看第一个else if的条件,如果第一个else if的条件为真(今天是星期二),就进入这个else if,否则就接着看第二个else if,第三个else if,直到最后一个else if。如果上面的else if统统都不成立,我们就进入最后一个else,执行“谢耳朵要自己做饭!”
关于上面的else if结构,有几点需要留意的地方:首先,在语法上else if可以有多个(当然也可以有0个,这时候就退化成了之前的if else);其次,最后的else也是可有可无的。你可以只带着N个else if结束这段程序。这时候如果if和所有的else if中的条件判断统统为假,那么程序什么也不做。
M脚下留心:
正文中给出的都只是伪代码,仅用于讲解语法,不是可以在电脑上运行的程序!请不要试图直接复制粘贴“谢耳朵要自己做饭”然后编译运行程序!
:动手体验:
回到我们为怪兽大学写的程序上来(忘记题目设定的读者可以查阅5.1.2节):
#include
 
int main()
{
int voice;
float score;
//从控制台读入一个整数
scanf("%d", &voice);
//计算考生得分
score = (float)voice / 40 + 2.5;
//输出考生的得分
printf("%f", score);
return 0;
}
郝刻薄院长发现一个问题:不管婴儿的分贝数有多么低,考生们至少都能拿到2.5分保底。正如她的名字显示的那样,郝刻薄院长是一个严厉的老师,她觉得2.5分对于那些懒惰的学生来说太宽容了,因此她决定着手修改量化评分标准:
如果婴儿的分贝低于60,小怪兽的得分为0;
如果婴儿的分贝不低于60而低于80,小怪兽得分为2.5分;
否则,按照原来的方法评分:得分=婴儿分贝/40+2.5。
由于怪兽大学没有计算机系,请你再次帮助郝刻薄院长完成这个程序。
switch判断结构
我们之前用if和else if为谢耳朵写了一小段管理每天菜单的代码。作为一个Geek,谢耳朵觉得如此多的else if堆在一起还是不够优美,因此他把你的程序改动成了这个样子:
int day;
scanf("%d", &day);
switch(day)
{
case 1:
printf("燕麦粥n");
break;
case 2:
printf("汉堡n");
break;
case 3:
printf("奶油土豆汤n");
break;
case 4:
printf("披萨n");
break;
case 5:
printf("法式面包n");
break;
case 6:
printf("中餐n");
break;
default:
printf("谢耳朵要自己做饭n");
}
在这里谢耳朵为我们介绍了新的语句——switch语句。这是一种升级版的if else语句,专门用于处理多种分支间的选择:
switch(表达式)
{
case 值1:
语句1;
break;
case 值2:
语句2;
break;
...
default:
语句N;
}
switch是相对比较复杂的语句,我们来详细解释一下:
首先,switch一开始会计算表达式的值。这个值可以是整型,可以是字符,可以是枚举,但是,不能是浮点数float或double。不严谨地说,只要这个值可以被解读成是一个整数,就可以用switch。
接下来,根据表达式的值,选择进入合适的case。如果表达式的值恰好符合某一个case后面的值,就跳到那个case当中,开始执行case下面的语句。需要强调的是,这里的语句1,语句2,语句N等都不一定是一行语句,它们可以是多行语句,而且不需要用花括号{}括起来。如果,表达式的值不符合任何一个case,那么就跳转到default,去执行default下面的语句N。正如在if-else-if当中最后的else可以没有一样,在switch中,最后的default也可以不写,这时如果表达式不符合任何一个case,就直接跳过整个switch代码,相当于什么都不做。
最后,也是switch当中最神奇最费解的,是每一个case内部都跟了一个break。我们将在循环结构中再次见到break语句。在这里,break语句表示当我们进入了某个case后,执行完了这个case下的语句之后,遇到了一句break,我们就跳出整个switch语句,这一段代码结束。如果,在某一个case下面没有break,那么当执行完了当前的case之后,程序会接着进入下一个case继续执行里面的代码,这个过程会一直持续到遇见第一个break为止。当然,如果接下来的case里面都没有break的话,那就一直执行到底,包括最后的default,然后跳出switch。
上面这几段话是非常抽象的,如果你看了一遍之后就完全明白了switch的执行顺序,那么恭喜你,你很适合学习编程!如果你看得一头雾水,没有关系,我们还是用谢耳朵的程序来说明一下:
int day;
scanf("%d", &day);
switch(day)
{
case 1:
printf("燕麦粥n");
break;
case 2:
printf("汉堡n");
break;
case 3:
printf("奶油土豆汤n");
break;
case 4:
printf("披萨n");
break;
case 5:
printf("法式面包n");
break;
case 6:
printf("中餐n");
break;
default:
printf("谢耳朵要自己做饭n");
}
今天是星期二,谢耳朵在控制台上输入了2。这个时候,整型变量day的值就是2。现在我们进入switch:很显然,case 2匹配上了我们的day,因此我们跳到case 2开始执行里面的语句:
case 2:
printf("汉堡n");
break;
我们首先执行printf:在控制台上,我们输出了“汉堡”,这一句完成。接下来,我们遇到了一个break。按照我们刚刚的讲解,遇到break之后我们就跳出整个switch语句,这一部分就结束了。
所以,当谢耳朵输入了2之后,整个switch语句的执行效果就是:在屏幕上打印了一句
“汉堡”,然后结束。谢耳朵输入其他case的值(1,3,4,5,6)效果是类似的。
如果今天是星期天,谢耳朵输入了7,此时day的值是7,那么在switch中我们挨个遍历所有的case,发现和day的值都不符合,于是我们最后就落脚在default上,执行printf,输出一句“谢耳朵要自己做饭”,然后switch这一段结束。
好,截止到目前为止,这个程序应该还是很清晰的。接下来我们来对谢耳朵的程序做一些小改动来说明break的作用:
int day;
scanf("%d", &day);
switch(day)
{
case 1:
printf("燕麦粥n");
break;
case 2:
printf("汉堡n");
case 3:
printf("奶油土豆汤n");
case 4:
printf("披萨n");
break;
case 5:
printf("法式面包n");
break;
case 6:
printf("中餐n");
default:
printf("谢耳朵要自己做饭n");
}
如果谢耳朵输入了1,那么结果和之前一样——在屏幕上打印一句“燕麦粥”,结束。
如果,谢耳朵输入的是2,这个时候程序的结果就不一样了——首先,我们会跳到case 2当中,在屏幕上输出“汉堡”。执行完了这句printf之后,我们发现没有break,于是程序会接着跳到case 3中,执行“奶油土豆汤”。接下来由于case 3中也没有break,于是程序继续向下执行,进入case,输出“披萨”,终于我们这次看到了break,于是执行完了case 4之后,跳出switch,结束。所以这时如果谢耳朵输入了2,他看到的结果将是:
2
汉堡
奶油土豆汤
披萨
:动手体验:
加上main函数和必要的头文件,在你的电脑上实现上一节的程序,输入6,猜猜输出会是什么?输出和你预想的一致吗?
循环结构
介绍完了顺序和选择结构之后。我们在本节向大家介绍循环结构。顾名思义,循环结构就是一遍又一遍地做重复的事情。我们还是用开车的例子来说明:在F1比赛中,10只车队的20位车手们驾驶着各自的赛车在一条数公里的环形赛道上风驰电掣,每场比赛中选手需要环绕赛道数十圈,最先跑完全程的车手获得冠军。假设在一次比赛中,车手们需要跑完50圈,利用此前学习的顺序和选择结构,我们可以用如下方式表示这个过程:
int count = 0;
if (count < 50)
{
printf ("我跑了一圈n");
count++;//count = 1
}
if (count < 50)
{
printf ("我跑了一圈n");
count++;//count = 2
}
...
当然,这样手工写50遍if语句是非常非常不可取的。幸运的是,C语言的表达能力不会如此低下,循环语句就可以帮助我们简化上述过程。
在本节中,我们首先向大家介绍for循环,它特别适合循环次数已知的情况;随后我们将介绍while循环和do while循环,这两种循环都比较适合应用在循环次数未知的情况。
for循环结构
for循环有可能是最常见的循环了,我们以后会经常和它打交道。for循环的语法如下:
for (表达式1;条件判断;表达式3)
{
表达式2;
}
for循环的执行过程是这样的:
1. 首先计算表达式1;
2. 判断条件是否为真,如果为假,跳出循环不再执行;
3. 如果条件判断为真,进入循环,执行表达式2;
4. 执行表达式3,转到步骤2。
我们来看一个具体的例子:
int sum = 0;
for (int i = 1; i <= 100; i++)
{
sum += i;
}
上述循环完成了求前100个自然数和的工作。如果你还没有反应过来,没有关系,我们可以把它拆开来一步一步执行:
1. 首先,i被赋值为1;
2. 条件判断:i<=100吗?正确,继续执行;
3. 执行sum+=i,此时sum=1;
4. 执行i++,此时i=2;
5. 再次进行条件判断:i<=100正确;
6. 执行sum+=i,此时sum=1+2;
7. 执行i++,此时i=3;
等等。我们跳过中间的过程,直接观察这个循环什么时候结束:假设某一次循环执行完i++之后i=100,我们进行条件判断i<=100为真,开始执行sum+=i,这时sum里面保存了1加到100的值。接下来,执行i++,i被修改为101了,此时不满足i<=100了,因此跳出循环。最终sum中的值为前100个自然数的和。
需要强调的是,如果在表达式1中定义了新变量i,那么这个变量只在循环内是可见的,也就是说只有在这个循环内可以使用变量i,当我们跳出了这个循环之后,程序就不认识i是什么了。如果你在循环外使用i的话:
int sum = 0;
for (int i = 1; i <= 100; i++)
{
sum += i;
}
printf("%dn", i);
你会发现编译无法通过——你的编译器不认识printf中的i是什么。当然如果你事先在循环体外部声明了i的话:
int sum = 0;
int i;
for (i = 1; i <= 100; i++)
{
sum += i;
}
printf("%dn", i);
那printf就能够认出需要输出的i是一个int类型的变量。
如果循环体当中只有一条语句,那么循环体的花括号可以省略。在上面的例子中,我们也可以把它精简成如下形式:
int sum = 0;
for (int i = 1; i <= 100; i++)
sum += i;
当然是否省略花括号只反映了不同的编程风格,并不会影响程序的结果。读者可以根据自己的喜好选择适合自己的编程风格。
M脚下留心:为什么我的循环程序不对?
如果一切正常的话,上述循环的执行结果是5050。然而,假设有一个粗心的程序员把代码敲成了这样:
int sum = 0;
for (int i = 1; i <= 100; i++);
{
sum += i;
}
你会发现你的程序编译无法通过了。我们查看VS的报错,发现是sum+=i当中的i没有定义。这说明sum+=i根本就不在循环体内——循环究竟在哪里呢?答案是:这是一个空循环!注意我们加在for语句后面的分号事实上形成了一条什么都不干的空语句:
for (int i = 1; i <= 100; i++)
{
;
}
{
sum += i;
}
所以整个for循环空转了100次,跟在最后的sum+=i只执行了一遍,而且还不知道i是个什么值,自然编译器就报错了。
while循环结构
我们之前说过for循环特别适合已知循环次数的情况。正如同上面求前100个自然数的和,我们知道循环100次就可以完成这件事。然而,在有些情况下我们事先无法知道循环次数,这时候使用for循环就不太自然(当然,并不是说for循环不可以完成这样的工作):
:动手体验:前多少个自然数的和大于1000?
我们已经学会了利用for循环求解前N个自然数的和,其中N是给定的整数。现在假设有某一个N满足前N个自然数的和大于1000,但是前N-1个自然数的和小于等于1000(你可以想象成当我们求和时,加到了N的时候,和刚刚好超过了1000)。你能用for循环实现这个程序吗?
:动手体验:猜数字
假设Foo和Bar两个人在玩猜数字的游戏。Foo在心中想了一个0到1000的整数(包括0和1000),Bar每次可以向Foo提问“你心里想的数大于XXX吗?”,Foo会给出真实的回答。假设Bar每次都采用二分的策略来提问,即每次都将Foo心中的数所在区间缩小一半。举个例子:
Bar:这个数大于500吗?Foo:对 ([501, 1000])
Bar:这个数大于750吗?Foo:错([501, 750])
Bar:这个数大于625吗?Foo:对([626, 750])
等等。如果我们用C语言来模拟上述过程的话,循环的次数(Bar猜测的次数)和Foo心中一开始想的数字有关,我们根本无法事先得知循环的次数。
对于上面这些不知道循环次数的情况,C语言提供了比for更合适的while语句来解决这个问题:
while (条件判断)
{
表达式;
}
while语句看上去要比for简洁不少:首先while会进行条件判断,如果为假就跳出循环,如果为真就进入循环体执行表达式,执行完之后继续条件判断,如果为假跳出,为真则进入循环体继续执行,等等。可以看出,while循环和下面的for循环是一致的:
for (;条件判断;)
{
表达式;
}
在这个特殊的for循环中,初始化语句为空,每次循环结束的更新语句也为空。
我们用本节开头的第一个例子来展示while循环的用法:
int sum = 0;
int i = 1;
while (sum <= 1000)
{
sum += i;
i++;
}
printf("%dn", i-1);
我们来顺着while循环的执行过程将上述程序走一遍:
1. 初始化sum和i;
2. 判断sum<=1000,正确;
3. sum+= i;此时sum=1;
4. i++;此时i=2;即下一次要加的数;
5. 判断sum<=1000,正确;
6. sum+=i,此时sum=1+2;
7. i++;此时i=3;
等等。这里跳出循环时i的值需要我们稍微推敲一下:当使得sum恰好超过1000的那个N出现时(即i=N时),此时sum保存了1到N-1的和,它是小于等于1000的,因此下一次循环开始时可以正常进入循环体。接下来sum加上了N(超过1000了),i被修改成了N+1,因此下一次条件判断就失败了,跳出循环。此时i中保存的是N+1的值,为了得到N我们需要输出i-1。
do-while循环
do-while循环是while循环的一种变体:
do
{
表达式;
}while(条件判断);
稍有不同的是,在do-while循环中,首先执行表达式,然后进行条件判断,如果为真,就回头继续执行表达式,否则就跳出。我们可以把do-while循环改写成如下的for循环:
for (表达式;条件判断;)
{
表达式;
}
do-while循环不像前两种循环那么常见,读者如果要使用do-while循环的话,请留心这一点:循环体中的表达式至少会被执行一次。
continue,break与return
在循环结构的最后,我们介绍continue,break和return,它们可以大大扩展循环结构的表达能力。
continue,break和return的直接作用都是中断当前的循环,不同的是,在此之后,continue会开始下一次循环;break会跳出循环,而return会直接跳出当前函数(我们目前的循环都是在main函数中,在main函数中执行return就相当于整个程序结束了)。我们以表达能力最丰富的for循环来举例:continue的作用如下:
for (表达式1;条件判断;表达式3)
{
表达式2-1;
continue;
表达式2-2;//不会被执行!
}
和正常的for循环一样,我们首先从表达式1开始,接着进行条件判断,如果为真,进入循环体执行表达式2-1,执行continue——这里出现了变化:我们遇到了continue之后,会中断本次循环(表达式2-2不被执行),跳去执行表达式3,然后接着进行条件判断,等等。在上面的for循环中,由于continue的存在,表达式2-2不会被执行。
如果把continue换成了break,那么执行到break之后就不仅仅是中止这一次循环了,而是直接跳出整个for循环,执行for循环之后的代码;如果continue换成了return,那么在main函数中程序直接结束。
可以看出不加限制地使用continue,break和return会让一些代码永远不被执行到,这显然是没有意义的。因此一般continue,break和return都会和条件判断语句结合使用——当满足一定条件时,跳过循环中剩下的代码,否则还继续正常执行。
我们用while循环来举一个例子:
int sum = 0;
while (true)
{
if (sum == 100)
break;
sum++;
printf("%dn", sum);
}
这段代码打印了前100个正整数(当然,你也可以用for循环100次来实现它),我们来单步跟踪一下它的执行过程:
1. 初始化sum;
2. 条件判断通过(这是个永真循环,条件判断永远为true);
3. 判断sum是否为100,结果为否,因此不执行break;
4. sum++;
5. printf;
6. 进行下一次条件判断(永真);
7. 判断sum是否为100,继续为否,不执行break;
8. sum++;
9. printf;
等等。直到sum等于100了,进入循环体内部之后我们的条件判断为真了,因此执行break,直接跳出循环,不再执行随后的sum++和printf。如果把break换成continue,那么sum等于100时执行continue,跳过随后的sum++和printf直接跳到条件判断语句true那里,开始下一次循环(当然你应该意识到在这里把break改成continue是有风险的——这个循环停不下来了!)
循环嵌套
无论是for循环,while循环还是do while循环,它们的内部都是可以嵌套新的循环的。最常见的例子就是多重for循环:
:动手体验:两重for循环
假设有人找你开发一个4人在线打牌的程序,需要你帮助他们实现一个非常简单的自动发牌的功能:4个人打两副扑克牌用108张,要求你用程序模拟正常的抽牌过程:
第1次:
玩家1抽一张
玩家2抽一张
玩家3抽一张
玩家4抽一张
第2次:
玩家1抽一张
玩家2抽一张
玩家3抽一张
玩家4抽一张
……
第27次:
玩家1抽一张
玩家2抽一张
玩家3抽一张
玩家4抽一张
他们给了你一个简单的函数:
void dealCards(int playerId)
{
printf("发一张牌给玩家%dn", playerId);
}
来体现给编号为playerId的玩家发一张牌的功能。比如,如果你在main函数中输入:
dealCards(1);
那么就会在屏幕上输出:发一张牌给玩家1。
试着运行下面的程序,猜猜它会输出什么?
#include
 
void dealCards(int playerId)
{
printf("发一张牌给玩家%dn", playerId);
}
 
int main()
{
for (int round = 1; round <= 27; round++)
{
printf("第%d轮n", round);
for (int id = 1; id <= 4; id++)
{
dealCards(id);
}
}
return 0;
}
两重for循环并不比简单的一重for循环要复杂多少。它只是在外层的for循环执行的每一步中完整地又执行了一个新的for循环而已。在上面这个简单的例子中,我们首先写了一个循环27次的外循环,用来模拟发牌的每一轮。在这个循环里,我们要模拟给四个玩家发牌的过程,直观地说,这样写会比较容易理解:
for (int round = 1; round <= 27; round++)
{
printf("第%d轮n", round);
dealCards(1);
dealCards(2);
dealCards(3);
dealCards(4);
}
在每一轮中,我们干了四件事:分别发牌给玩家1到4号。但是,如果你仔细观察,你会发现这四句话也是一个天然的循环结构:我们只需要用一个循环来遍历1到4这4个自然数就可以了!于是我们又有了第二重循环:
for (int round = 1; round <= 27; round++)
{
printf("第%d轮n", round);
for (int id = 1; id <= 4; id++)
{
dealCards(id);
}
}
除了两重for循环,你也可以在for循环里嵌套while,或者在while循环里嵌套for,等等。你还可以写出更多重的for循环。不过,如果一个程序员的程序里有连续四五重for循环的话,这段循环很有可能会非常费解,尤其是如果里面还包含了条件判断,continue,break等等语句的话。因此,如果你写出了这样的多重for循环,最好还是考虑把你的程序稍微修改一下。
goto语句
在循环结构的最后,我们介绍一种古老的语句:goto语句。goto语句用来在程序内进行跳转,它具有非常大的灵活性。我们以之前的发牌机为例:
#include
 
void dealCards(int playerId)
{
printf("发一张牌给玩家%dn", playerId);
}
 
int main()
{
int round = 1;
start:
printf("第%d轮n", round);
dealCards(1);
dealCards(2);
dealCards(3);
dealCards(4);
round++;
if (round <= 27)
goto start;
return 0;
}
这个程序和之前的发牌机程序输出结果是一样的。虽然程序中没有任何循环结构的关键字,但是它实际上执行了一段循环。我们来逐一讲解它和此前的for循环的不同之处:
首先,在进入main函数之后,我们定义了一个round变量,并初始化为1。这一步和for循环的初始化是一样的。
随后,我们定义了一个叫做start的标签:
start:
这个标签没有任何实际含义,只是用来给随后的goto语句定位。我们暂时先忽略它。
接下来程序打印了当前的轮次:
printf("第%d轮n", round);
并给四个玩家发牌:
dealCards(1);
dealCards(2);
dealCards(3);
dealCards(4);
发牌结束后,我们把round的值自增1:
round++;
这一步是不是很像for循环中的更新语句?
接下来我们去执行一个条件判断:
if (round <= 27)
goto start;
这句话的意思是:如果round还没有超过27轮,我们就跳到到start标签的位置继续执行。也就是说,执行完goto之后,我们继续回到:
printf("第%d轮n", round);
开始执行,此时的round已经自增为2了,我们接着执行发牌,自增round,判断的过程,知道round自增到了28,这个时候if判断不通过,我们不用goto了,而是直接执行return跳出程序。
如果你把整个程序完整地走一遍,你会发现它其实在干和for循环一样的事情,但是,使用循环语句比使用goto要好懂多了。在大多数情况下,我们强烈建议初学者谨慎使用goto语句。
本章小结
C语言的语句以分号结尾,用花括号括起来的一系列语句被称为一个语句块。
变量分为局部变量和全局变量。变量必须先定义再使用。局部变量的作用域从定义开始到函数结束为止。在语句块内的变量会使得语句块之外的同名变量失效。
顺序结构是最常见的程序结构。printf和scanf用来完成基本的输入输出功能。使用scanf的时候需要传入变量的地址。
判断结构包括if和switch两种。if之后可以带else if或else。如果遇到分支数特别多的情况请使用switch,记得在每个分支结束前加上break。
循环结构包括for循环,while循环和do while循环。for循环适合循环次数已知的情况,while循环适合循环次数未知的情况。如果要求循环必须执行至少一次,请使用do while循环。continue用于执行下一次循环,break用于跳出循环,return可以跳出整个函数。循环之内可以嵌套循环。goto语句用于在程序内进行跳转,慎用goto语句。

喜欢0 评分0
游客

返回顶部