第3章 格式化输入/输出
scanf函数和printf函数是C语言编程中使用最频繁的两个函数,它们用来格式化输入和输出。正如本章要展示的那样,虽然这两个函数功能强大,但要用好它们却不容易。
3.1 printf函数
在C语言编程中,printf
函数扮演着输出信息的重要角色。它不仅能够显示文本,还能将变量的值嵌入到这些文本中。本文将详细介绍如何使用 printf
函数,并指出一些常见的使用误区。
printf
函数的核心功能是将格式化的字符串和变量值结合起来,输出到控制台。使用时,你需要提供一个格式字符串,后面跟着你希望显示的变量或值。
格式字符串: 格式字符串由普通字符和特殊的转换说明组成。普通字符会直接显示,而转换说明则作为占位符,用来插入变量的值。例如,%d
是一个转换说明,它告诉 printf
将一个整数转换成十进制形式并显示出来。
示例: 假设我们有以下变量赋值:
int i = 10, j = 20;
float x = 43.2892f, y = 5527.0f;
使用 printf
函数输出这些变量的值:
printf("i = %d, j = %d, x = %f, y = %f\n", i, j, x, y);
这将产生如下输出:
i = 10, j = 20, x = 43.289200, y = 5527.000000
在这个例子中,%d
和 %f
作为转换说明,分别替换为变量 i
、j
、x
和 y
的值。
注意事项:
- 匹配问题:
printf
函数不会自动检查格式字符串中的转换说明数量是否与提供的参数数量一致。如果转换说明过多或过少,可能会导致输出不正确或显示无意义的值。 - 类型不匹配: 如果转换说明与数据类型不匹配,
printf
仍然会尝试输出,但结果可能没有意义。例如,使用%f
来输出一个整数,或者相反。
常见错误:
- 转换说明数量与参数不匹配:
printf("%d %d\n", i); // 错误:只有一个参数,但有两个转换说明
- 参数顺序与转换说明不匹配:
printf("%f %d\n", i, x); // 错误:第一个参数是整数,但使用了浮点数的转换说明
结语: 正确使用 printf
函数对于C语言编程至关重要。理解格式字符串和转换说明的概念,以及它们与变量值的匹配,可以帮助你避免常见的错误,确保程序输出的准确性。
3.1.1 转换说明
转换说明给程序员提供了大量对输出格式的控制方法。另一方面,转换说明很可能很复杂且难以阅读。
- 转换说明可以包含格式化信息。具体来说,我们可以用%.1f来显示小数点后带一位数字的float型值。
- 最小字段宽度(minimum field width)m指定了要显示的最少字符数量。如果要显示的数值所需的字符数少于m,那么值在字段内是右对齐的。
在C语言中,printf
函数的精度(precision)参数 p
确实与所使用的转换说明符(conversion specifier)紧密相关。以下是对精度 p
的含义和它如何与不同转换说明符配合使用的详细解释:
-
%d
—— 表示十进制整数。精度p
指定了要显示的数字的最少位数。如果实际数字的位数少于p
,那么会在数字前面填充零以达到指定的位数。如果省略精度参数,它默认为1,即只显示必要的位数,不填充零1。 -
%e
—— 表示科学记数法形式的浮点数,例如1.2345e+02
。精度p
指定了小数点后的位数。默认情况下,如果没有指定精度,它为6位。如果精度为0,并且没有使用#
标记符,那么小数点将不会显示1。 -
%f
—— 表示定点十进制形式的浮点数,例如123.45
。精度p
与%e
中的含义相同,即指定小数点后的位数。默认精度也是6位1。 -
%g
—— 表示根据数值的大小,选择指数形式或定点十进制形式中较短的一种来显示浮点数。精度p
指定了可以显示的有效数字的最大数量,不包括小数点后的零。如果数值没有小数点后的数字,%g
不会显示小数点,与%f
不同,%g
不会显示尾随的零1。
请注意,精度参数 p
可以是一个具体的数字,也可以是一个星号 *
,表示精度值将由下一个参数提供。例如,printf("%.*f", precision, value);
中 precision
指定了小数点后的位数。
这些规则确保了 printf
函数在格式化输出时的灵活性和准确性,允许程序员根据需要精确控制输出的格式。
想象一下,你正在编写一个程序,需要显示一系列数值,但这些数值的大小和范围可能会有很大的变化。这时,printf
函数中的%g
转换说明符就显得尤为重要了。它能够根据数值的大小自动选择最合适的显示方式,无论是定点十进制还是科学记数法。
下面是一个简单的C语言程序,它展示了如何使用printf
函数以不同的格式来显示整数和浮点数。
#include <stdio.h>
int main(void) {
int number; // 整数变量
float price; // 浮点数变量,模拟商品价格
number = 123; // 假设这是商品编号
price = 99.99f; // 假设这是商品价格
// 使用不同的格式说明符来显示数值
printf("商品编号: |%d| 宽度自适应\n", number);
printf("商品编号: |%5d| 宽度至少5个字符\n", number);
printf("商品编号: |%-5d| 左对齐,宽度至少5个字符\n", number);
printf("商品编号: |%05d| 宽度至少5个字符,不足部分用0填充\n", number);
printf("商品价格: |%10.2f| 宽度10个字符,保留两位小数\n", price);
printf("商品价格: |%10.2e| 宽度10个字符,科学记数法,保留两位小数\n", price);
printf("商品价格: |%-10g| 左对齐,宽度10个字符,根据数值大小自动选择格式\n", price);
return 0;
}
输出示例:
商品编号: |123| 宽度自适应
商品编号: | 123| 宽度至少5个字符
商品编号: |123 | 左对齐,宽度至少5个字符
商品编号: |00123| 宽度至少5个字符,不足部分用0填充
商品价格: | 99.99| 宽度10个字符,保留两位小数
商品价格: | 9.99e+01| 宽度10个字符,科学记数法,保留两位小数
商品价格: |99.99 | 左对齐,宽度10个字符,根据数值大小自动选择格式
通过这个例子,我们可以看到printf
函数如何灵活地处理不同大小的数值。无论是商品编号还是价格,printf
都能够以一种清晰、美观的方式将它们展示出来。下次当你需要格式化输出时,不妨试试printf
的强大功能吧!
注意: 格式串中的|
字符在这里仅作为分隔符,帮助我们更清楚地看到每个数值占用的空间,它本身对printf
函数没有特殊含义。
3.1.2转义序列
在C语言的编程世界里,有些字符拥有特殊的力量,它们能够控制程序的流程,或者在输出时产生特殊的效果。这些特殊的字符被称为转义序列。今天,就让我们一起揭开它们的神秘面纱。
转义序列是一些以反斜杠\
开始的字符组合,它们告诉编译器:“嘿,我不是普通的字符,我是有特殊任务的!”下面,让我们来认识一些常见的转义序列朋友:
\a
—— 警报(响铃)符。在大多数机器上,它会让电脑发出一声短暂的鸣响,就像是有人在耳边轻轻说:“注意啦!”\b
—— 回退符。它会像时光倒流一样,让光标回到上一个位置,仿佛是按下了“后退”键。\t
—— 水平制表符。它能够优雅地将光标移动到下一个制表位,就像是在表格中跳到了下一个单元格。\n
—— 换行符。它让光标跳到下一行的起始位置,就像是按下了“回车”键。
这些转义序列在printf
函数的格式串中扮演着重要的角色。例如,下面的printf
语句:
printf("Item\tUnit\tPurchase\n\tPrice\tDate\n");
执行后,会显示出一个整洁的两行标题:
Item Unit Purchase Price Date
看!转义序列\t
和\n
就像是排版助手,帮助我们轻松地创建表格和列表。
还有一个特殊的转义序列\"
,它用来表示双引号"
字符。因为在C语言中,双引号用来标记字符串的开始和结束,所以当我们需要在字符串中包含一个双引号时,就需要用到这个转义序列。例如:
printf("\"Hello!\"");
这将输出:
"Hello!"
注意: 如果你在字符串中只放置一个反斜杠\
,编译器会认为它后面应该跟着一个转义序列。如果你想要显示一个单独的反斜杠,就需要在字符串中使用两个反斜杠\\
:
printf("\\\\"); /* 打印出一个 \ 字符 */
结语: 转义序列是C语言中一个非常有用的工具,它们让我们能够以一种非常直观和灵活的方式来控制输出的格式和内容。下次当你需要在程序中使用这些特殊字符时,记得使用这些隐身斗篷哦!
3.2 scanf函数
在C语言的编程世界里,scanf
函数是我们与用户交互的桥梁。它不仅能够理解用户的输入,还能将这些输入转化为程序能够理解的数据。但就像所有的桥梁一样,使用不当可能会导致沟通的断裂。让我们深入了解scanf
的使用方法,避免那些常见的陷阱。
3.2.1 scanf函数的工作方法
实际上scanf函数可以做的事情远远多于目前为止已经提到的这些。scanf函数本质上是一种“模式匹配”函数,试图把输入的字符组与转换说明相匹配。
scanf
函数通过格式串来指导其读取输入的行为。在调用时,它从格式串的起始位置开始,逐个处理转换说明。对于每个转换说明,scanf
会在输入中搜索符合要求的数据项,并在发现不匹配的字符时停止读取。如果数据项成功读取,scanf
将继续处理后续的转换说明;如果读取失败,它将停止处理并返回。
scanf
在搜索数据项时会忽略所有空白字符,包括空格、制表符、换行符等,这使得输入的数字可以在同一行内或者分散在多行中。例如,对于以下scanf
调用:
scanf("%d%d%f%f", &i, &j, &x, &y);
scanf
函数在处理用户输入时,会将多行输入视为一个连续的字符序列。它能够跳过所有空白字符,包括空格、制表符、换页符和换行符,以寻找和读取数值数据。以下是scanf
函数处理输入数据的规则和行为的总结:
-
连续字符流:
scanf
将多行输入视为连续的字符流,忽略行与行之间的换行符。 -
跳过空白:在定位数值的起始位置时,
scanf
会跳过所有空白字符。 -
读取数值:
- 对于整数,
scanf
首先寻找正号或负号,然后连续读取数字直到遇到非数字字符。 - 对于浮点数,
scanf
会寻找可选的正负号,一串可能包含小数点的数字,以及可能跟随的指数部分(由字母e
或E
、可选的符号和数字序列组成)。
- 对于整数,
-
格式说明符通用性:
%e
、%f
和%g
作为浮点数的转换说明符,在scanf
中可以互换使用,它们遵循相同的识别规则。 -
字符回退:如果
scanf
在读取过程中遇到无法匹配当前格式说明符的字符,它会将该字符放回输入流中,留待后续处理或下一次调用。 -
输入项匹配:
scanf
尝试将格式串中的每个转换说明与输入数据项匹配。如果某项匹配失败,scanf
将停止处理剩余的格式串和输入数据,并立即返回。 -
换行符处理:
scanf
函数在一次调用中不会读取换行符,该字符将保留在输入流中,作为下一次调用的起始字符。
通过这些规则,scanf
能够灵活地处理各种格式的输入数据,但同时也要求程序员在使用时必须注意格式串与预期输入的匹配,以避免潜在的错误和程序崩溃。
3.2.2 格式串中的普通字符
scanf
函数在处理格式串时,会根据格式串中的普通字符和转换说明来执行模式匹配。以下是对scanf
函数处理格式串中普通字符的行为的总结:
-
空白字符匹配:
- 当格式串中出现空白字符(如空格、制表符等)时,
scanf
会从输入中连续读取空白字符,直到遇到第一个非空白字符,并将该非空白字符放回输入流。 - 格式串中的一个空白字符可以匹配输入中的任意数量(包括零个)的空白字符。
- 当格式串中出现空白字符(如空格、制表符等)时,
-
非空白字符匹配:
- 格式串中的非空白字符将与输入中的下一个字符进行比较。
- 如果字符匹配,
scanf
会继续处理格式串中的下一个项目。 - 如果字符不匹配,
scanf
会将该字符放回输入流,并停止处理当前的格式串,退出函数。
-
格式串与输入匹配示例:
- 如果格式串是
"%d/%d"
,输入是"5/96"
,scanf
将匹配两个整数,中间的斜杠由格式串中的斜杠字符匹配。 - 如果输入在数字和斜杠之间有额外的空格,如
"5 /96"
,scanf
会在第一个数字后跳过空格,但由于格式串中没有额外的空格来匹配输入中的空格,scanf
将停止在斜杠处,并将剩余的输入留给下一次调用。
- 如果格式串是
-
格式串设计:
- 为了使
scanf
正确处理输入中的空格,格式串应适当地包含或忽略空白字符。 - 如果希望在格式串中忽略空格,可以使用
"%d /%d"
这样的格式串,其中空格是格式串的一部分,允许输入中相应位置有空格。
- 为了使
通过理解scanf
函数如何处理格式串中的普通字符,我们可以更准确地设计输入解析逻辑,确保程序能够正确地读取和解析用户输入的数据。
3.2.3 易混淆的printf函数和scanf函数
虽然scanf函数调用和printf函数调用看起来很相似,但两个函数之间有很大的差异,忽略这些差异就是拿程序的正确性来冒险。
-
模式匹配能力:
scanf
函数能够识别和读取符合特定格式的输入数据。例如,它可以直接读取形如“分子/分母”的分数,而不需要分别对分子和分母进行单独的读取。 -
函数调用差异:
scanf
和printf
虽然在格式串的使用上相似,但它们的功能和行为有显著差异。错误地将printf
的用法套用到scanf
上,或者反之,都可能导致程序出错。 -
常见错误:
- 在
printf
中错误地使用&
操作符,如printf("%d", &i);
,这将导致输出不正确的地址值而非变量的实际值。 - 错误地假设
scanf
的格式串应与printf
的格式串相同,可能会导致输入解析不符合预期。
- 在
-
格式串设计:
scanf
通常会跳过输入中的空白字符,因此在格式串中通常不需要显式包含空白字符。- 格式串中的非空白字符必须与输入中的相应字符匹配,否则
scanf
会中止读取。
-
输入解析示例: 假设我们使用
scanf
读取两个整数,格式串为"%d,%d"
。如果用户输入5, 9
,scanf
会成功匹配并读取两个数字。但如果用户输入5 9
(没有逗号),scanf
会在读取第一个数字后,由于找不到逗号而提前终止。 -
换行符处理:
- 在
scanf
的格式串中使用换行符\n
通常不是一个好主意,因为它会与空白字符一样被scanf
忽略,可能导致程序等待用户输入下一个非空白字符。
- 在
-
程序示例: 考虑一个简单的程序,它读取用户的姓名和年龄,并显示欢迎消息:
#include <stdio.h> int main(void) { char name[50]; int age; printf("Enter your name: "); scanf("%s", name); // 正确地读取字符串,不需要& printf("Enter your age: "); scanf("%d", &age); // 正确地读取整数,需要& printf("Welcome, %s! You are %d years old.\n", name, age); return 0; }
在这个示例中,
scanf
使用%s
来读取字符串,使用%d
来读取整数,并且正确地在变量前使用了&
。我们可以看到scanf
在处理输入时的灵活性和强大功能,同时也需要注意正确地设计格式串,以确保程序的健壮性和正确性。
第4章 表达式
C语言以其对表达式的重视而著称,其中表达式是计算值的公式,可以是简单的变量和常量,也可以是更复杂的,由运算符和操作数构成的结构。C语言提供了丰富的运算符,包括基本的算术运算符(加、减、乘、除),关系运算符(用于比较),以及逻辑运算符(用于组合比较条件)。尽管C语言的运算符种类繁多,需要逐步学习和掌握,但这种多样性对于精通C语言至关重要。简而言之,C语言的表达式构建和运算符使用是其核心特性之一,对于深入理解和有效使用该语言来说必不可少。
4.1 算术运算符
算术运算符在C语言中扮演着基础而关键的角色,它们使得基本的数学运算得以实现。这些运算符包括用于加法、减法、乘法和除法的二元运算符,以及用于一元正负号的一元运算符。值得注意的是,加法作为一元运算符在传统C语言中并不执行任何操作,主要用来标记数值的正性。而求余运算符%
则计算两个数相除后的余数,这是其他编程语言中不太常见的。
在进行算术运算时,需要特别注意的是,当操作数都是整数时,除法运算符/
会执行整数除法,舍弃小数部分,而求余运算符%
则要求操作数必须为整数。此外,不能将零作为除数或模数,因为这会导致未定义的行为。对于负数的除法和取模运算,C89和C99标准规定了不同的行为,C89标准下结果可能向上或向下取整,而C99标准下结果总是向零取整。
运算符优先级和结合性对于理解复杂表达式的求值顺序至关重要。在C语言中,乘法和除法的优先级高于加法和减法,而所有二元算术运算符都是左结合的。这意味着在没有圆括号的情况下,相同优先级的运算符将从左到右依次计算。
例如,考虑计算一个简单的数学题目,如a + b * c
,根据运算符优先级,这将被解释为(a + b) * c
。如果需要改变这一默认顺序,可以使用圆括号来明确指定。
下面是一个程序示例,它演示了如何使用算术运算符来计算一个数值的累积和:
#include <stdio.h>
int main(void)
{
int num, sum = 0;
printf("Enter numbers to sum (non-numeric to quit): ");
while (scanf("%d", &num) == 1)
{
sum += num; // 使用加法和赋值运算符
}
printf("The sum is: %d\n", sum);
return 0;
}
在这个程序中,我们使用+=
运算符来更新累积和sum
。用户可以连续输入数字,直到输入一个非数字字符来结束输入。程序随后输出所有输入数字的总和。这个例子展示了算术运算符在实际程序中的简单应用。
4.2 赋值运算符
求出表达式的值以后常常需要将其存储到变量中,以便将来使用。C语言的=(简单赋值(simple assignment))运算符可以用于此目的。为了更新已经存储在变量中的值,C语言还提供了一种复合赋值(compound assignment)运算符。
4.2.1 简单赋值
在C语言中,赋值表达式v = e
的目的是计算表达式e
的值,并将该值复制给变量v
。这个操作可以涉及常量、变量或者复杂的表达式。例如:
i = 5;
使得变量i
的值变为5。j = i;
将i
的当前值复制给j
,现在j
也是5。k = 10 * i + j;
计算表达式的值,即10 * 5 + 5
,结果55赋给k
。
如果赋值两边的类型不一致,C语言会自动进行类型转换。例如:
int i; float f;
定义了整型变量i
和浮点型变量f
。i = 72.99f;
将浮点数72.99转换为整型并赋给i
,i
现在是72。f = 136;
将整型136转换为浮点数并赋给f
,f
现在是136.0。
在C语言中,赋值不仅是一个操作,它本身也是一个运算符,并且其结果就是赋值的结果。这意味着i = 72.99f;
表达式的值是72,而不是72.99。
赋值运算符还有副作用,即改变其左侧操作数的值。例如,i = 0;
不仅计算表达式的值为0,还改变了i
的值。
C语言允许多个赋值表达式串联在一起,如i = j = k = 0;
。这种串联赋值是右结合的,意味着表达式会先计算最右边的值,然后依次向左赋值。因此,上述表达式等价于i = (j = (k = 0));
,即先给k
赋值0,再将k
的值赋给j
,最后将j
的值赋给i
。
然而,类型转换可能导致串联赋值的结果与预期不符,如float f; int i; f = i = 33.3f;
首先将33.3f赋值给i
(截断为33),然后将i
的值(33)转换为浮点数赋给f
,结果f
是33.0而非33.3。
此外,虽然可以在表达式中使用赋值运算符,如k = j = i + 1;
,但这种做法可能会降低代码的可读性,并可能引入错误。因此,建议避免在复杂表达式中使用嵌入式赋值。
4.2.2 左值
在C语言中,几乎所有运算符都能处理变量、常量或包含运算符的复杂表达式作为它们的操作数。但是,赋值运算符有所不同,它要求其左侧操作数必须是左值。左值是指在内存中占有存储空间的对象,它可以是变量,也可以是更复杂的结构,但一定不是常量或表达式的计算结果。
由于左值必须是一个存储位置,这意味着赋值运算符的左侧不能是常量或无存储位置的表达式。例如:
12 = i;
这是不合法的,因为数字12不是左值,它没有内存地址。i + j = 0;
这也是不合法的,i + j
是一个表达式,不是一个左值。-i = j;
同样不合法,-i
是一个表达式,不是左值。
如果尝试将赋值运算符用于非左值的左侧,编译器会报错,通常会提示“invalid lvalue in assignment”(赋值中无效的左值)。
记住,赋值运算符左侧必须是可以存储值的变量或可修改的位置,这是C语言中赋值操作的基本要求。
4.3 自增运算符和自减运算符
在C语言中,频繁地基于变量的当前值进行计算并更新该变量是一种常见的做法。例如,将变量i
增加2的操作可以通过i = i + 2;
实现。为了简化这类操作,C语言提供了复合赋值运算符,它将加法和赋值合并为一个步骤。
使用+=
运算符,上述语句可以被简写为i += 2;
,这与i = i + 2;
等效。这种写法不仅代码更简洁,而且阅读起来也更直观。除了+=
,C语言还提供了其他九种复合赋值运算符,如-=
、*=
、/=
和%=
,它们的工作机制都是类似的,分别用于实现减法、乘法、除法和取余的复合赋值操作。
在使用复合赋值运算符时,需要特别注意不要将运算符的两部分颠倒,这会导致表达式的含义发生错误。例如,i += j
和i =+ j
在语法上都是合法的,但它们的含义完全不同。i += j
将j
的值加到i
上,而i =+ j
实际上是i = (+j)
的简写,只执行了将j
的值赋给i
,丢失了预期的加法操作。
复合赋值运算符具有与简单赋值运算符相同的特性,它们都是右结合的。这意味着在遇到多个复合赋值操作时,应该从右向左进行计算。例如,表达式i += j += k;
应该理解为i += (j += k);
,即先计算j += k
的结果,然后再将这个结果加到i
上。
总的来说,复合赋值运算符是C语言中提高编码效率和表达能力的重要工具,但使用时需要准确理解它们的含义,避免因错误的使用导致程序逻辑出错。
4.4 表达式求值
在C语言中,自增(++
)和自减(--
)运算符提供了一种简洁的方式来对变量进行加一或减一的操作。这些运算符不仅可以作为后缀使用,也可以作为前缀使用,它们的位置不同,行为也有所不同。
- 后缀形式 (
i++
或j--
):在表达式的值被使用后,变量的值才增加或减少。例如,printf("%d", i++);
会打印出i
的原始值,然后i
的值增加1。 - 前缀形式 (
++i
或--j
):变量的值在表达式被使用前首先增加或减少。例如,printf("%d", ++i);
会先将i
的值增加1,然后打印出新值。
这种区别意味着,如果你在一个表达式中同时使用++
和--
,它们的顺序将影响最终的结果。前缀形式的++
和--
与一元正负号有相同的优先级,并且是右结合的,而后缀形式的++
和--
的优先级比一元正负号高,并且是左结合的。
例如,考虑以下代码片段:
i = 1;
j = 2;
k = ++i + j++; // 这里++i是前缀形式,j++是后缀形式
执行后,i
增加到2,k
被赋值为i
的新值加上j
的原始值,即4。然后j
增加到3。所以最终i
、j
和k
的值分别是2、3和4。
如果代码是:
i = 1;
j = 2;
k = i++ + j++; // 这里i++和j++都是后缀形式
那么k
将被赋值为i
和j
的原始值之和,即3。然后i
增加到2,j
增加到3。所以最终i
、j
和k
的值分别是2、3和3。
使用自增和自减运算符时,要注意它们改变变量值的副作用,以及它们在表达式中的位置如何影响这些变化的时机。不当的使用可能导致难以理解的代码和预期之外的结果。
在C语言中,运算符的优先级和结合性规则允许我们理解表达式的结构,但它们并不总是能确定表达式的求值顺序。C语言标准没有规定大多数子表达式的求值顺序,这意味着编译器可能会以任何顺序求值这些子表达式。
例如,在表达式(a + b) * (c - d)
中,我们无法预知子表达式(a + b)
和(c - d)
哪一个先被计算。大多数情况下,这不会对表达式的结果产生影响。但是,如果子表达式中包含对同一变量的修改,那么求值顺序就会变得重要,并可能导致不同的结果。
考虑以下代码:
a = 5; c = (b = a + 2) - (a = 1);
这里,c
的最终值取决于子表达式的求值顺序。如果(b = a + 2)
先计算,b
将为7,然后a
变为1,c
将为6。反之,如果(a = 1)
先计算,b
将为3,c
将为2。这种不确定性是未定义行为的一个例子。
为了避免这种问题,最佳实践是避免在表达式中使用赋值或其他可能改变变量值的操作。改写上述代码,我们得到:
a = 5; b = a + 2; a = 1; c = b - a;
这样,每个变量的值在任何时候都是明确的,避免了因求值顺序不确定而导致的问题。
自增(i++
)和自减(--i
)运算符也需要注意,因为它们改变了操作数的值。例如:
i = 2; j = i * i++;
这里,j
的值是4还是6取决于i++
是先取i
的原始值还是新值。这种不确定性同样是未定义行为。
未定义行为是编程中应当避免的,因为它可能导致程序编译失败、运行不稳定或产生不可预测的结果。C语言标准建议程序员避免编写可能导致未定义行为的代码,确保程序的可移植性和可靠性。
4.5 表达式语句
C语言允许将任何表达式转换成语句,只需在表达式后添加一个分号。这种做法虽然在语法上是合法的,但并不总是有意义。以下是一些相关的例子和注意事项:
-
自增表达式作为语句:
++i;
这条语句将i
的值增加1,但由于表达式没有被赋值或以其他方式使用,增加的值被丢弃了。不过,i
本身的值确实发生了变化。 -
副作用的重要性: 当表达式用作语句时,如果它没有副作用(比如修改变量的值),那么这个语句可能没有实际效果。例如:
i--;
这里i
的值减少了1,但由于结果没有被使用,这个操作可能看起来没有意义。 -
无用的计算:
i * j - 1;
这个语句计算了i * j - 1
的值,但由于没有将结果赋值或用于其他目的,这个计算实际上没有对程序产生任何影响。 -
避免无意义的语句: 在编写代码时,有时可能会不小心写出没有实际效果的表达式语句。例如,本来想写赋值语句:
i = j;
却错误地写成了:i + j;
这种情况下,编译器可能会发出警告,提示这是一个没有效果的语句。 -
编译器警告: 某些编译器能够识别出这类无意义的表达式语句,并给出警告,如“statement with no effect”,提示程序员可能存在代码错误。
总结来说,虽然C语言允许将任何表达式作为语句使用,但除非表达式具有副作用,否则这种做法可能不会对程序产生任何实际效果。编写代码时,应注意避免创建这样的无用语句,确保每一行代码都有明确的意图和效果。
第5章 选择语句
C语言的语句结构相对简单,主要包括以下几种类型:
-
表达式语句:任何表达式后跟一个分号构成表达式语句,如
i++
或i = j + 1;
。 -
选择语句:
if
语句和switch
语句用于基于条件选择不同的执行路径。 -
重复语句:
while
、do-while
和for
循环允许重复执行一段代码。 -
跳转语句:
break
、continue
和goto
语句允许程序流程发生跳转,return
语句也视为跳转语句的一种。 -
复合语句:使用花括号
{}
将多个语句组合在一起,作为一个单一的语句执行。 -
空语句:只包含一个分号的语句,不执行任何操作。
本节将重点讨论选择语句和复合语句。选择语句使用逻辑表达式来测试条件,这些逻辑表达式可以通过关系运算符(如 <
、<=
、>
、>=
)、相等运算符(==
、!=
)和逻辑运算符(&&
、||
、!
)来构造。例如,if
语句可以这样使用:
if (条件) { // 条件为真时执行的代码 } else { // 条件为假时执行的代码 }
复合语句允许我们将多个语句组织在一起,如下所示:
{ 语句1; 语句2; // 更多语句... }
这种结构在编写需要多个步骤或多条语句才能完成的功能时非常有用。
5.1 逻辑表达式
5.1.1 关系运算符
C语言中的关系运算符用于比较两个值,并根据比较结果返回真(1)或假(0)。这些运算符包括:
<
(小于)>
(大于)<=
(小于或等于)>=
(大于或等于)==
(等于)!=
(不等于)
这些运算符在C语言中的行为与数学中的相应符号相同,但它们的结果是以整数形式表示的。例如,10 < 11
的结果是1,表示真;而11 < 10
的结果是0,表示假。
关系运算符可以用于比较整数和浮点数,也可以用于混合类型的比较。例如,1 < 2.5
的结果是1,表示真,因为1确实小于2.5;而5.6 < 4
的结果是0,表示假,因为5.6不小于4。
关系运算符的优先级低于算术运算符,这意味着在没有圆括号的情况下,算术运算会先于关系比较进行。例如,在表达式i + j < k - 1
中,实际上是先计算(i + j)
和(k - 1)
,然后比较它们的大小。
关系运算符是左结合的,这意味着当多个关系运算符连续出现时,会从左到右进行求值。例如,表达式i < j < k
实际上是((i < j) < k)
。这并不会检查j是否在i和k之间,而是先比较i和j,然后将结果(1或0)与k进行比较。
如果想要检查j是否在i和k之间,应该使用逻辑与运算符&&
,如下所示:
i < j && j < k
这个表达式首先检查i是否小于j,如果是,再检查j是否小于k,两个条件都为真时,整个表达式的结果才为真。
了解关系运算符的这些特性对于编写逻辑正确且易于理解的C语言程序至关重要。
5.1.2 判等运算符
在C语言中,关系运算符用于比较两个值的关系,而判等运算符用于检查两个值是否相等或不相等。以下是C语言中使用的判等运算符:
==
(等于):如果两边的值相等,则结果为1(真),否则为0(假)。!=
(不等于):如果两边的值不相等,则结果为1(真),否则为0(假)。
由于赋值运算符是单个=
字符,为了避免混淆,C语言使用两个=
字符来表示“等于”运算符,而“不等于”运算符则由!=
表示。
判等运算符的优先级低于关系运算符,这意味着在没有圆括号的情况下,关系运算符会先被计算。例如,在表达式i < j == j < k
中,实际上是先计算(i < j)
和(j < k)
,然后再比较这两个结果是否相等。
判等运算符是左结合的,这意味着当它们连续出现时,会从左到右进行求值。例如,a == b == c
实际上是(a == b) == c
。
一些程序员可能会利用判等运算符返回的整数值来编写简洁的代码,例如,使用(i >= j) + (i == j)
来确定变量i
和j
之间的关系,并根据这个关系返回0、1或2。然而,这种编码技巧可能会使代码难以理解和维护,因此通常不推荐使用。
为了提高代码的可读性,建议使用明确的逻辑表达式,例如:
if (i < j)
{
// i 小于 j 时的代码
}
else if (i > j)
{
// i 大于 j 时的代码
}
else
{
// i 等于 j 时的代码
}
这样的代码结构清晰,易于理解和维护。
5.1.3 逻辑运算符
在C语言中,逻辑运算符允许我们构建复杂的逻辑表达式,这些表达式可以包含简单的布尔值或更复杂的条件。以下是C语言中的逻辑运算符:
!
(非):这是一个一元运算符,用于反转布尔值。如果表达式的值为0(假),它返回1(真);如果表达式的值为非0(真),它返回0(假)。&&
(与):这是一个二元运算符,当且仅当两个操作数都为真(非0值)时,结果才为真(1)。它是短路运算符,如果第一个操作数为假(0),则不会计算第二个操作数。||
(或):这是一个二元运算符,如果两个操作数中至少有一个为真(非0值),结果就为真(1)。它也是短路运算符,如果第一个操作数为真,就不会计算第二个操作数。
逻辑运算符的结果和操作数可以是任何整数值,但只有0和非0值被当作假和真来处理。以下是逻辑运算符的一些规则:
!表达式
:如果表达式的结果为0,则!表达式
的结果为1;否则为0。表达式1 && 表达式2
:只有当两个操作数都非零时,结果才为1。如果第一个操作数为0,整个表达式的结果为0,且不会评估第二个操作数(短路计算)。表达式1 || 表达式2
:如果任一操作数非零,结果就为1。如果第一个操作数非零,整个表达式的结果为1,且不会评估第二个操作数(短路计算)。
短路计算的特性可以用于避免潜在的错误,例如在上面的(i != 0) && (j / i > 0)
表达式中,如果i
为0,就不会计算(j / i > 0)
,从而避免除以零的错误。
然而,短路计算也可能影响表达式的副作用。例如,在表达式i > 0 && ++j > 0
中,如果i
不大于0,那么++j
不会被执行,这意味着j
的值不会因为这个表达式而增加。这可能会让代码的行为变得难以预测,因此在编写包含逻辑运算符的表达式时,需要谨慎考虑操作数的副作用。
总的来说,逻辑运算符是构建条件逻辑和控制程序流程的重要工具,但它们也需要程序员有清晰的理解和正确的使用,以避免产生意外的结果。
5.2 if语句
5.2.1 复合语句
复合语句(也称为块语句)是由花括号{}
包围的一系列语句。在if
语句中,复合语句定义了满足条件时执行的代码块。
if (条件) { // 条件为真时执行的代码块 }
5.2.2 else子句
else
子句提供了一个与if
条件相反的执行路径。如果if
条件不满足,程序将执行else
子句中的代码块。
if (条件) { // 条件为真时执行的代码块 }
else { // 条件为假时执行的代码块 }
5.2.3 级联式if语句
级联式if
语句允许你根据多个条件进行选择。每个else if
子句提供了额外的条件来检查。
if (条件1)
{ // 条件1为真时执行的代码块 }
else if (条件2) { // 条件2为真时执行的代码块 }
else { // 所有条件都不满足时执行的代码块 }
5.2.4 “悬空else”的问题
当if
语句嵌套使用时,else
子句可能与预期的if
条件不匹配,这被称为“悬空else”问题。为了避免混淆,建议在每个if
和else
子句后都使用花括号。
5.2.5 条件表达式
条件表达式(也称为三元运算符)是一种简洁的方式来根据条件选择两个值之一。
result = (条件) ? 值1 : 值2;
5.2.6 C89中的布尔值
在C89标准中,布尔值不是原生类型。程序员通常使用宏定义(如#define TRUE 1
)或枚举类型来模拟布尔值。
5.2.7 C99中的布尔值
C99标准引入了_Bool
关键字和stdbool.h
头文件,提供了原生的布尔类型bool
,以及true
和false
宏定义。
5.3 switch语句
switch
语句用于基于不同的情况执行不同的代码块。每个case
标签对应一个特定的值,而default
标签是可选的,用于处理没有匹配的case
的情况。
switch (表达式)
{
case 值1: // 当表达式等于值1时执行的代码块 break;
case 值2: // 当表达式等于值2时执行的代码块 break;
// ...
default: // 没有匹配的case时执行的代码块
}
switch
语句的执行流程是:计算表达式的值,然后查找匹配的case
标签。找到匹配的case
后,执行该标签下的代码块,直到遇到break
语句。break
用于退出switch
语句;如果没有break
,程序将继续执行下一个case
的代码块,这称为“fall through”。
了解if
和switch
语句的用法对于编写能够根据不同条件执行不同逻辑的C语言程序至关重要