文章目录
C现代方法笔记(chapter3&4)
第3章 格式化输入/输出
3.1 printf函数
-
格式串包含普通字符和转换说明(conversion specification),其中转换说明以字符
%
开头。转换说明是用来表示打印过程中待填充的值的占位符。跟随在字符%
后边的信息指定了把数值从内部形式(二进制)转换成打印形式(字符)的方法,这就是“转换说明”这一术语的由来。 -
例如,转换说明
%d
指定printf
函数把int
值从二进制形式转换成十进制数字组成的字符串,转换说明%f
对float
型值也进行类似的转换。
3.1.1 转换说明
-
一般地,转换说明可以用
%m.pX
格式或%-m.pX
格式,这里的m
和p
都是整型常量,而X
是字母。m
和p
都是可选的。-
最小栏宽(minimum field width)
m
指定了要显示的最少字符数量。如果要显示的数值所需的字符数少于m
,那么值在字段内是右对齐的。(换句话说,在值前面放置额外的空格。)例如,转换说明%4d
将以·123
的形式显示数123
(本章用符号·
表示空格字符)。如果要显示的值所需的字符数多于m
,那么栏宽会自动扩展为所需的尺寸。因此,转换说明%4d
将以12345
的形式显示数12345
,而不会丢失数字。在m
前放上一个负号会导致左对齐;转换说明%-4d
将以123·
的形式显示123
。 -
精度(precision)
p
的含义很难描述,因为它依赖于转换指定符(conversion specifier)X
的选择。X
表明在显示数值前需要对其进行哪种转换。对数值来说最常用的转换指定符有以下几个。d
——表示十进制(基数为10)形式的整数。p
指明了待显示数字的最少个数(必要时在数前加上额外的零);如果省略p,则默认它的值为1。e
——表示指数(科学记数法)形式的浮点数。p
指明了小数点后应该出现的数字个数(默认值为6)。如果p
为0,则不显示小数点。f
——表示“定点十进制”形式的浮点数,没有指数。p
的含义与说明符e
中的一样。g
——表示指数形式或者定点十进制形式的浮点数,形式的选择根据数的大小决定。p
意味着可以显示的有效数字(不是小数点后的数字)的最大数量。与转换指定符f
不同,g
的转换将不显示尾随的零。此外,如果要显示的数值没有小数点后的数字,g
就不会显示小数点。
-
下面是一个示例程序,说明了用printf
函数以各种格式显示整数和浮点数的方法。
/*tprintf.c
Prints int and float values in various formats
*/
#include <stdio.h>
int main(int argc, char* argv[]){
int i;
float x;
i = 40;
x = 839.21f;
printf("|%d|%5d|%-5d|%5.3d|\n", i, i, i, i);
printf("|%10.3f|%10.3e|%-10g|\n", x, x, x);
return 0;
}
//输出:
|40| 40|40 | 040|
| 839.210| 8.392e+02|839.21 |
3.1.2 转义序列
- 警报(响铃)符:
\a
。 - 回退符 :
\b
。 - 换行符 :
\n
。 - 水平制表符:
\t
。
当这些转义序列出现在printf
函数的格式串中时,它们表示在显示中执行的操作。在大多数机器上,输出\a
会产生一声鸣响,输出\b
会使光标从当前位置回退一个位置,输出\n
会使光标调到下一行的起始位置,输出\t
会把光标移动到下一个制表符的位置。
printf("\\"); //prints one \ character
3.2 scanf函数
int i, j;
scanf("%d%d%f%f", &i, &j, &x, &y);
假设用户录入了下列输入行:
1 -20 .3 -4.0e3
scanf
函数将读入上述行的信息,并且把这些符号转换成它们表示的数,然后分别把1
、-20
、0.3
和-4000.0
赋值给变量i
、j
、x
和y
。scanf
函数调用像"%d%d%f%f"
这样“紧密压缩”的格式串是很普遍的,而printf
函数的格式串很少有这样紧挨着的转换说明。
3.2.1 scanf函数的工作方法
scanf
函数本质上是一种“模式匹配”函数,试图把输入的字符组与转换说明相匹配。
- 像
printf
函数一样,scanf
函数是由格式串控制的。 - 调用时,
scanf
函数从左边开始处理字符串中的信息。 - 对于格式串中的每一个转换说明,
scanf
函数从输入的数据中定位适当类型的项,并在必要时跳过空格。 scanf
函数读入数据项,并且在遇到不可能属于此项的字符时停止。如果读入数据项成功,那么scanf
函数会继续处理格式串的剩余部分。- 如果某一项不能成功读入,那么
scanf
函数将不再查看格式串的剩余部分(或者余下的输入数据),并立即返回。
scanf
函数遵循什么规则来识别整数或浮点数呢?
当要求读入整数时,scanf
函数首先寻找正号或负号,然后读取数字,直到读到一个非数字时才停止。当要求读入浮点数时,scanf
函数会寻找一个正号或负号(可选),随后是一串数字(可能含有小数点),再往后是一个指数(可选)。指数由字母e(或者字母E)、可选的符号,以及一个或多个数字构成。在用于scanf
函数时,转换说明%e
、%f
和%g
是可以互换的,这3种转换说明在识别浮点数方面都遵循相同的规则。
3.2.2 格式串中的普通字符
- 空白字符。当在格式串中遇到一个或多个连续的空白字符时,
scanf
函数从输入中重复读空白字符,直到遇到一个非空白字符(把该字符“放回原处”)为止。格式串中空白字符的数量无关紧要,格式串中的一个空白字符可以与输入中任意数量的空白字符相匹配。(附带提一下,在格式串中包含空白字符并不意味着输入中必须包含空白字符。格式串中的一个空白字符可以与输入中任意数量的空白字符相匹配,包括零个。) - 其他字符。当在格式串中遇到非空白字符时,scanf 函数将把它与下一个输入字符进行比较。如果两个字符相匹配,那么scanf 函数会放弃输入字符,并继续处理格式串。如果两个字符不匹配,那么scanf 函数会把不匹配的字符放回输入中,然后异常退出,而不进一步处理格式串或者从输入中读取字符。
3.2.3 易混淆的printf函数和scanf函数
- 第一个常见错误是关于
printf
函数的,调用时,在变量前面放置&
。比如,printf("%d %d\n", &i, &j); /*** WRONG **
。 - 在寻找数据项时,
scanf
函数通常会跳过空白字符。因此除了转换说明,格式串通常不需要包含字符。
下面是一个分数相加的程序例子:
/*
Adds two fractions
*/
#include <stdio.h>
int main(int argc, char* argv[]){
int num1, denom1, num2, denom2, result_num, result_denom;
printf("Enter first fraction: ");
scanf("%d/%d", &num1, &denom1);
printf("Enter second fraction: ");
scanf("%d/%d", &num2, &denom2);
result_num = num1 * denom2 + num2 * denom1;
result_denom = denom1 * denom2;
printf("The sum is %d/%d\n", result_num, result_denom);
return 0;
}
//输出:
Enter first fraction: 4/5
Enter second fraction: 6/7
The sum is 58/35
问与答
答:在printf
格式串中使用时,二者没有区别。但是,在scanf
格式串中,%d
只能与十进制(基数为10
)形式的整数相匹配,而%i
则可以匹配用八进制(基数为8
)、十进制或十六进制(基数为16
)表示的整数。如果输入的数有前缀0
(如056
),那么%i
会把它作为八进制数( 7.1 节)来处理;如果输入的数有前缀0x
或0X
(如0x56
),那么%i
会把它作为十六进制数( 7.1 节)来处理。如果用户意外地将0
放在数的开始处,那么用%i
代替%d
读取数可能有意想不到的结果。因为这是一个陷阱,所以建议坚持采用%d
。
答:如果printf
函数在格式串中遇到两个连续的字符%
,那么它将显示出一个字符%
。例如,语句printf("Not profit: %d%%\n", profit);
可以显示出Not profit: 10%
。
答:不可能知道。打印\t
的效果不是由C语言定义的,而是依赖于所使用的操作系统。水平制表符之间的距离通常是8个字符宽度,但C语言本身无法保证这一点。
答:不会把非数值输入存到变量中,而是留给下一次scanf
函数调用。如何处理这种糟糕的情况呢?后面将看到检测scanf
函数调用是否成功( 22.3 节)的方法。如果调用失败,可以终止或者尝试恢复程序,可能的方法是丢掉有问题的输入并要求用户重新输入。
答:我们知道,用户从键盘输入时,程序并没有读取输入,而是把用户的输入放在一个隐藏的缓冲区中,由scanf
函数来读取。scanf
函数把字符放回到缓冲区中供后续读取是非常容易的。第22章会更详细地讨论输入缓冲。
答:除非格式串中本来就有标点符号,不然的话,scanf
函数会立刻返回,把标点符号和之后的数留给下一次scanf
函数调用。
编程题
- 可以使用getchar()函数来获取输入流的一个字符
- getchar()函数可以检测回车,一旦回车,将会把输入的字符输出到键盘缓冲区当中。
- getchar()函数读入第一个字符后,输入流的读取位置会向后移动一位,这时再用其他函数,比如scanf函数,读取的位置就不是从头开始了。
第4章 表达式
运算符是构建表达式的基本工具,C语言拥有异常丰富的运算符。首先,C 语言提供了基本运算符,这类运算符存在于大多数编程语言中。
- 算术运算符,包括加、减、乘和除。
- 关系运算符,进行诸如“
i
比0
大”这样的比较运算。 - 逻辑运算符,实现诸如“
i
比0
大并且i
比10
小”这样的关系运算。但是C 语言不只包括这些运算符,还提供了许多其他运算符。
4.1 算术运算符
算术运算符又分为一元运算符和二元运算符。
- 一元运算符:
+
(正号),-
(负号)。 - 二元运算符:
+
(加法)、-
(减法)、*
(乘法)、/
(除法)、%
(求余)。
除%
运算符以外,二元运算符既允许操作数是整数也允许操作数是浮点数,两者混合也是可以的。当把int
型操作数和float
型操作数混合在一起时,运算结果是float
型的。因此,9+2.5f
的值为11.5
,而6.7f/2
的值为3.35
。
- 运算符
/
可能产生意外的结果。当两个操作数都是整数时,运算符/
会丢掉分数部分来“截取”结果。因此,1 / 2
的结果是0
而不是0.5
。 - 运算符
%
要求操作数是整数。如果两个操作数中有一个不是整数,程序将无法编译通过。 - 把
0
用作/
或%
的右操作数会导致未定义的行为。 - 当运算符
/
和运算符%
用于负操作数时,其结果难以确定。根据C89标准,如果两个操作数中有一个为负数,那么除法的结果既可以向上舍入也可以向下舍入。(例如,-9/7
的结果既可以是-1
也可以是-2
)在C89中,如果i
或者j
是负数,i%j
的符号与具体实现有关。(例如,-9%7
的值可能是-2
或者5
。)但是在C99中,除法的结果总是趋零截尾的(因此-9/7
的结果是-1
),i%j
的值的符号与i
的相同(因此-9%7
的值是-2
)。
关于运算符的优先级和结合性:
- 加减乘除的优先级跟数学四则运算的优先级是一样的;
- 加减乘除和求余运算都是从左向右结合的,而一元运算都是右结合的;
- 虽然优先级和结合性规则非常重要,然而C语言的运算符太多了(差不多50种),因此,学会参考运算符表,或者加上足够多的圆括号。
4.2 赋值运算符
求出表达式的值以后,通常需要将其存储到变量中,以便将来使用。C 语言的=
[简单赋值(simple assignment)]运算符可以用于此目的。为了更新已经存储在变量中的值,C语言还提供了一种复合赋值(compound assignment)运算符。
4.2.1 简单赋值
i = 5; /* i is now 5 */
j = i; /* j is now 5 */
k = 10 * i + j; /* k is now 55 */
int i;
float f;
i = 72.99f; /* i is now 72 */
f = 136; /* f is now 136.0 */
类型转换的问题( 7.4 )以后再讨论。
多个赋值可以串联在一起,比如i = j = k = 0;
,由于=
是右结合的,因此这个表达式的作用是先把0
赋值给k
,再把表达式k = 0
的值赋值给j
,最后把表达式j = (k = 0)
的值赋值给i
。
int i;
float f;
f = i = 33.3f;
//首先把数值33赋值给变量i,然后把33.0(而不是预期的33.3)赋值给变量f。
4.2.2 左值
既然赋值运算符要求左操作数是左值,那么在赋值表达式的左侧放置任何其他类型的表达式都是不合法的:
12 = i; /*** WRONG ***/
i + j = 0; /*** WRONG ***/
-i = j; /*** WRONG ***/
编译器会检测出这种错误,并给出 invalid lvalue in assignment
这样的出错消息。
4.2.3 复合赋值
+=
运算符把右操作数的值加到左侧的变量中去。如下所示:
i += 2; /* same as i = i + 2; */
- 还有另外9种复合赋值运算符,包括
-=
、*=
、/=
、%=
。
所有复合赋值运算符的工作原理大体相同。
4.3 自增运算符和自减运算符
还有一点就是,后缀++
和后缀--
比一元的正号和负号优先级高,而且这两个后缀都是左结合的。前缀++
和前缀--
与一元的正号和负号优先级相同,而且这两个前缀都是右结合的。
4.4 表达式求值
有时候表达式的值可能依赖于子表达式的求值顺序。但是C语言没有定义子表达式的求值顺序【除了含有逻辑与运算符及逻辑或运算符( 5.1 节)、条件运算符( 5.2 节)以及逗号运算符( 6.3 节)的子表达式】。比如:在表达式(a + b) * (c - d)
中,无法确定子表达式(a + b)
是否在子表达式(c – d)
之前求值。
a = 5;
c = (b = a + 2) – (a = 1);
第二条语句的执行结果是未定义的,C标准没有规定。对大多数编译器而言,c
的值是6
或者2
。如果先计算子表达式(b = a + 2)
,那么b
的值为7
,c
的值为6
。但是,如果先计算子表达式(a = 1)
,那么b
的值为3
,c
的值为2。
a = 5;
b = a + 2;
a = 1;
c = b – a;
总而言之,类似这样未定义的,后果是不可预料的语句,我们应该避免使用。
4.5 表达式语句
C语言有一条不同寻常的规则,那就是任何表达式都可以用作语句。换句话说,不论表达式是什么类型、计算什么结果,我们都可以通过在后面添加分号将其转换成语句。例如,可以把表达式++i
转换成语句:
++i;
执行这条语句时,i
先进行自增,然后把新产生的i
值取出(与放在表达式中的效果一样)。但是,因为++i
不是更长的表达式的一部分,所以它的值会被丢弃,执行下一条语句。(当然,对i
的改变是持久的。)
因为会丢掉++i
的值,所以除非表达式有副作用,否则将表达式用作语句并没有什么意义。一起来看看下面的3个例子。在第一个例子中,i
存储了1
,然后取出i
的新值,但是未使用:
i = 1;
在第二个例子中,取出i
的值但没有使用,随后i
进行自减:
i--;
在第三个例子中,计算出表达式i * j – 1
的值后丢弃:
i * j – 1;
因为i
和j
没有变化,所以这条语句没有任何作用。
问与答
答:通过重复乘法运算,可以进行较为简单的整数次幂运算(i * i * i
是i
的立方运算)。如果想计算非整数次幂,可以调用pow
函数。
答:%
运算符要求操作数是整数,这种情况下可以试试fmod
函数。
答:规则其实不像看起来那么复杂。C89和C99都要确保(a / b) * b + a % b
的结果总是等于a
(事实上,只要a / b
的值是可表示的,C89和C99标准就都能确保这一点)。问题在于C89中,a / b
和a % b
有两种情况可满足这一相等性:-9 / 7
为-1
且-9 % 7
为-2
,或者-9 / 7
为-2
且-9 % 7
为5
。在第一种情况下,(-9 / 7) * 7 + -9 % 7
的值为-1 × 7 + -2 = -9;
在第二种情况下,(-9 / 7) * 7 + -9 % 7
的值为-2 × 7 + 5 = -9
。C99出现的时候,大多数CPU 都将除法的结果趋零截尾,所以这也被写入这一标准作为唯一允许的结果。
答:是的,当然。不过在C语言里不叫右值,C语言中的“值”就是“右值”,都是指“表达式的值”。只有左值才可能放在赋值运算符的左侧,否则它就是一个值,或者说右值。当然,C语言不需要“右值”这个概念,C标准也不使用这个概念,这是其他语言,比如C++才使用的概念。
答:计算v += e
只会求一次v
的值,而计算v = v + e
会求两次v
的值。在后一种情况下,对v
求值可能引起的任何副作用也都会出现两次。在下面的例子中,i
只自增一次:
a[i++] += 2;
如果用=
代替+=
,语句变成
a[i++] = a[i++] + 2;
i
的值在别处被修改和使用了,因此上述语句的结果是未定义的。i
的值可能会自增两次,但我们无法确定到底会发生什么。
答:C语言从Ken Thompson
早期的B语言中继承了++
和--
。Thompson
创造这类运算符是因为他的B语言编译器可以对++i
产生比i = i + 1
更简洁的翻译。这些运算符已经成为C语言根深蒂固的组成部分(事实上,许多最著名的C语言惯用法都依赖于这些运算符)。对于现代编译器而言,使用++
和--
不会使编译后的程序变得更短小或更快,继续普及这些运算符主要是由于它们的简洁和便利。
答:可以。自增和自减运算也可以用于浮点数,但实际应用中极少采用自增和自减运算符处理float
型变量。
答:这是一个非常好的问题,也是一个非常难回答的问题。C语言标准引入了“序列点”的概念,并且指出“应该在前一个序列点和后一个序列点之间对存储的操作数的值进行更新”。在C语言中有多种不同类型的序列点,表达式语句的末尾是其中一种。在表达式语句的末尾,该语句中的所有自增和自减操作都必须执行完毕,否则不能执行下一条语句。
在后面章节中会遇到的一些运算符(逻辑与、逻辑或、条件和逗号)对序列点也有影响。函数调用也是如此:在函数调用执行之前,所有的实际参数必须全部计算出来。如果实际参数恰巧是含有++
或--
运算符的表达式,那么必须在调用前进行自增或自减操作。
答:根据定义,一个表达式表示一个值。例如,如果i
的值为5
,那么计算i + 1
产生的值为6
。在末尾添加分号,把i+1
变成语句:
i + 1;
执行这条语句时,我们计算出了i + 1
的值,但是我们没有保存这个值(也没有以某种方式使用这个值),因此这个值就丢失了。
总结
本文是作者阅读《C语言程序设计:现代方法(第2版·修订版)》时所做笔记,也是第一篇书籍笔记文章,日后会推出后续章节笔记。感谢各位大佬批评指正,希望对诸位有所帮助,Thank you very much!!