printf函数被设计用来显示格式串(format string),并且在该串中的指定位置插入可能的值。
显示的值可以是常量、变量或者更加复杂的表达式。
格式串包含普通字符和转换说明(conversion specification),其中转换说明以字符%
开头。转换说明是用来表示打印过程中待填充的值的占位符。跟随在字符%
后边的信息指定了把数值从内部形式(二进制)转换成打印形式(字符)的方法,这就是“转换说明”这一术语的由来。
例如,转换说明%d
指定printf
函数把int
值从二进制形式转换成十进制数字组成的字符串,转换说明%f
对float
型值也进行类似的转换。
请注意,C语言编译器不会检测格式串中转换说明的数量是否和输出项的数量相匹配。
此外,C语言编译器也不检测转换说明是否适合要显示项的数据类型。如果程序员使用不正确的转换说明,程序将简单的产生无意义的输出。
转换说明给程序员提供了大量对输出格式的控制方法。
一般地,转换说明可以用%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
就不会显示小数点。编写程序时无法预知数的大小或者数值变化范围很大的情况下,说明符
g
对于数的显示是特别有用的。在用于显示大小适中的数时,说明符g
采用定点十进制形式。但是,在显示非常大或非常小的数时,说明符g
会转换成指数形式以便减少所需的字符数。除了
%d
、%e
、%f
和%g
以外,还有许多其他的说明符,后续的章节会讲到。
下面是一个示例程序,说明了用printf
函数以各种格式显示整数和浮点数的方法。
/*tprintf.c
Prints int and float values in various formats
*/
#include
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 |
格式串中常用的代码
\n
被称为转义序列(escape sequence)。转义序列使字符串包含一些特殊数字符而不会使编译器引发问题,这些字符包括非打印的(控制)字符和对编译器有特殊含义的字符(如“
)。先看一组示例。
\a
。\b
。\n
。\t
。当这些转义序列出现在printf
函数的格式串中时,它们表示在显示中执行的操作。在大多数机器上,输出\a
会产生一声鸣响,输出\b
会使光标从当前位置回退一个位置,输出\n
会使光标调到下一行的起始位置,输出\t
会把光标移动到下一个制表符的位置。
为了显示单独一个字符
\
,需要在字符串中放置两个字符\
:
printf("\\"); //prints one \ character
如同printf函数,scanf函数也根据特定的格式读取输入。函数的格式串也可以包含普通字符和转换说明两个部分。转换说明用法上,两个函数本质上是一样的。
在许多情况下,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
函数的格式串很少有这样紧挨着的转换说明。
像
prinf
函数一样,scanf
函数也有一些不易觉察的陷阱。使用scanf
函数时,程序员必须检查转换说明的数量是否与输入变量的数量相匹配,并且检查每个转换是否适合相对应的变量。与printf
函数一样,编译器无法检查出可能的匹配不当。另一个陷阱与符号&
有关,符号&
通常被放在scanf
函数调用中每个变量的前面。符号&
常常(但不总是)是需要的,记住使用它是程序员的责任。如果
scanf
函数调用中忘记在变量前面放置符号&
,将产生不可预知甚至可能是毁灭性的结果。程序崩溃是常见的结果。最轻微的后果则是从输入读进来的值无法存储到变量中,变量将保留原有的值(如果没有给变量赋初始值,那么这个原有值可能是没有意义的)。忽略符号&
是极为常见的错误,一定要小心!一些编译器可以检查出这种错误,并产生一条类似“format argument is not a pointer”
的警告消息。
调用
scanf
函数虽然有效但是不理想,许多专业的C程序员会避免使用scanf
函数,而是采用字符格式读取所有数据,然后再把它们转化成数值形式。
scanf
函数本质上是一种“模式匹配”函数,试图把输入的字符组与转换说明相匹配。
printf
函数一样,scanf
函数是由格式串控制的。scanf
函数从左边开始处理字符串中的信息。scanf
函数从输入的数据中定位适当类型的项,并在必要时跳过空格。scanf
函数读入数据项,并且在遇到不可能属于此项的字符时停止。如果读入数据项成功,那么scanf
函数会继续处理格式串的剩余部分。scanf
函数将不再查看格式串的剩余部分(或者余下的输入数据),并立即返回。简单的说,如果碰到不匹配的输入,
scanf
就会立即返回,不会继续了。在寻找数的起始位置时,
scanf
函数会忽略空白字符(white-space character,包括空格符、水平和垂直制表符、换页符和换行符)。因此,我们可以把数字放在同一行或者分为几行来输入。
scanf
函数遵循什么规则来识别整数或浮点数呢?
当要求读入整数时,scanf
函数首先寻找正号或负号,然后读取数字,直到读到一个非数字时才停止。当要求读入浮点数时,scanf
函数会寻找一个正号或负号(可选),随后是一串数字(可能含有小数点),再往后是一个指数(可选)。指数由字母e(或者字母E)、可选的符号,以及一个或多个数字构成。在用于scanf
函数时,转换说明%e
、%f
和%g
是可以互换的,这3种转换说明在识别浮点数方面都遵循相同的规则。
当
scanf
函数遇到一个不可能属于当前项的字符时,它会把此字符“放回原处”,以便在扫描下一个输入项或者下一次调用scanf
函数时再次读入。
通过编写含有普通字符和转换说明的格式串能进一步地理解模式匹配的概念。处理格式串中的普通字符时,scanf函数采取的动作依赖于这个字符是否为空白字符。
scanf
函数从输入中重复读空白字符,直到遇到一个非空白字符(把该字符“放回原处”)为止。格式串中空白字符的数量无关紧要,格式串中的一个空白字符可以与输入中任意数量的空白字符相匹配。(附带提一下,在格式串中包含空白字符并不意味着输入中必须包含空白字符。格式串中的一个空白字符可以与输入中任意数量的空白字符相匹配,包括零个。)printf
函数的,调用时,在变量前面放置&
。比如,printf("%d %d\n", &i, &j); /*** WRONG **
。scanf
函数通常会跳过空白字符。因此除了转换说明,格式串通常不需要包含字符。通常情况下,不要在
scanf
格式串末尾放置换行符。
下面是一个分数相加的程序例子:
/*
Adds two fractions
*/
#include
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
问1:转换说明
%i
也可以用于读写整数。%i
和%d
之间有什么区别?
答:在printf
格式串中使用时,二者没有区别。但是,在scanf
格式串中,%d
只能与十进制(基数为10
)形式的整数相匹配,而%i
则可以匹配用八进制(基数为8
)、十进制或十六进制(基数为16
)表示的整数。如果输入的数有前缀0
(如056
),那么%i
会把它作为八进制数( 7.1 节)来处理;如果输入的数有前缀0x
或0X
(如0x56
),那么%i
会把它作为十六进制数( 7.1 节)来处理。如果用户意外地将0
放在数的开始处,那么用%i
代替%d
读取数可能有意想不到的结果。因为这是一个陷阱,所以建议坚持采用%d
。
问2:如果
printf
函数将%
作为转换说明的开始,那么如何显示字符%
呢?
答:如果printf
函数在格式串中遇到两个连续的字符%
,那么它将显示出一个字符%
。例如,语句printf("Not profit: %d%%\n", profit);
可以显示出Not profit: 10%
。
问3:转义序列
\t
会使printf
函数跳到下一个水平制表符处。如何知道水平制表符到底跳多远呢?
答:不可能知道。打印\t
的效果不是由C语言定义的,而是依赖于所使用的操作系统。水平制表符之间的距离通常是8个字符宽度,但C语言本身无法保证这一点。
问4:如果要求读入一个数,而用户录入了非数值的输入,那么
scanf
函数会如何处理?
答:不会把非数值输入存到变量中,而是留给下一次scanf
函数调用。如何处理这种糟糕的情况呢?后面将看到检测scanf
函数调用是否成功( 22.3 节)的方法。如果调用失败,可以终止或者尝试恢复程序,可能的方法是丢掉有问题的输入并要求用户重新输入。
问5:我不能理解
scanf
函数如何把字符“放回原处”并在以后再次读取。
答:我们知道,用户从键盘输入时,程序并没有读取输入,而是把用户的输入放在一个隐藏的缓冲区中,由scanf
函数来读取。scanf
函数把字符放回到缓冲区中供后续读取是非常容易的。第22章会更详细地讨论输入缓冲。
问6:如果用户在两个数之间加入了标点符号(如逗号),
scanf
函数将如何处理?
答:除非格式串中本来就有标点符号,不然的话,scanf
函数会立刻返回,把标点符号和之后的数留给下一次scanf
函数调用。
代码在
chapter3
文件夹。记录一下编程中领悟到的东西:
表达式是表示如何计算值的公式。最简单的表达式是变量和常量。变量表示程序运行时需要计算的值,常量表示不变的值,更加复杂的表达式把运算符用于操作数(操作数自身就是表达式)。表达式=变量(常量)+ 运算符。
比如在表达式
a+(b*c)
中,运算符+
用于操作数a
和(b*c)
。二这两者自身又都是表达式。
运算符是构建表达式的基本工具,C语言拥有异常丰富的运算符。首先,C 语言提供了基本运算符,这类运算符存在于大多数编程语言中。
i
比0
大”这样的比较运算。i
比0
大并且i
比10
小”这样的关系运算。但是C 语言不只包括这些运算符,还提供了许多其他运算符。算术运算符又分为一元运算符和二元运算符。
+
(正号),-
(负号)。+
(加法)、-
(减法)、*
(乘法)、/
(除法)、%
(求余)。除%
运算符以外,二元运算符既允许操作数是整数也允许操作数是浮点数,两者混合也是可以的。当把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标准故意对C语言的部分内容未加指定,并认为其细节可以由“实现”来具体定义。所谓实现是指程序在特定的平台上编译、链接和执行所需要的软件。因此,根据实现的不同,程序的行为可能会稍有差异。C89 中运算符/和运算符%对负操作数的行为就是一个由实现定义行为的例子。
最好避免编写依赖于由实现定义的行为的程序。如果不可能做到,那么起码要仔细查阅手册——C标准要求在文档中说明由实现定义的行为。
关于运算符的优先级和结合性:
小知识:为了读取单个的数字,我们使用带有
%1d
转换说明的scanf
函数,其中%1d
匹配只有1
位的整数。
求出表达式的值以后,通常需要将其存储到变量中,以便将来使用。C 语言的=
[简单赋值(simple assignment)]运算符可以用于此目的。为了更新已经存储在变量中的值,C语言还提供了一种复合赋值(compound assignment)运算符。
表达式
v = e
的赋值效果是求出表达式e
的值,并把此值赋值到v
。如下面的例子所示,e
可以是常量、变量或更为复杂的表达式:
i = 5; /* i is now 5 */
j = i; /* j is now 5 */
k = 10 * i + j; /* k is now 55 */
如果
v
和e
的类型不同,那么赋值运算发生时会把e
的值转换为v
的类型:
int i;
float f;
i = 72.99f; /* i is now 72 */
f = 136; /* f is now 136.0 */
类型转换的问题( 7.4 )以后再讨论。
请注意!在许多编程语言中,赋值是语句;然而,在C语言中,赋值就像
+
那样是运算符。换句话说,赋值操作产生结果,就如同两个数相加产生结果一样。赋值表达式v=e
的值就是赋值运算后v
的值。因此,表达式i=72.99f
的值72
(不是72.99
)。
多个赋值可以串联在一起,比如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。
大多数C语言运算符允许它们的操作数是变量、常量或者包含其他运算符的表达式。然而,赋值运算符要求它的左操作数必须是左值(lvalue)。左值表示对象,而不是常量或计算的结果。变量是左值,而诸如
10
或2 * i
这样的表达式则不是左值。目前为止,变量是已知的唯一左值。在后面的章节中,我们将介绍其他类型的左值。
既然赋值运算符要求左操作数是左值,那么在赋值表达式的左侧放置任何其他类型的表达式都是不合法的:
12 = i; /*** WRONG ***/
i + j = 0; /*** WRONG ***/
-i = j; /*** WRONG ***/
编译器会检测出这种错误,并给出 invalid lvalue in assignment
这样的出错消息。
+=
运算符把右操作数的值加到左侧的变量中去。如下所示:i += 2; /* same as i = i + 2; */
-=
、*=
、/=
、%=
。请注意,由于运算符优先级的问题,
v += e
不一定“等价于”v = v + e
。比如:表达式i *= j + k
和表达式i = i * j + k
是不一样的。类似的说明也使用于其他复合赋值运算符。复合赋值运算符有着和
=
运算符一样的特性。具体来说,它们都是右结合的,因此语句i += j += k;
意味着i += (j += k);
。
C语言允许用
++
(自增)和--
(自减)运算符将这些语句缩得更短些。
++i
意味着“立即自增i
”,i++
意味着“现在先用i
的原始值,稍后再自增i
”。这个“稍后”有多久呢?C语言标准没有给出精确的时间,但是可以放心地假设i
将在下一条语句执行前进行自增。
还有一点就是,后缀++
和后缀--
比一元的正号和负号优先级高,而且这两个后缀都是左结合的。前缀++
和前缀--
与一元的正号和负号优先级相同,而且这两个前缀都是右结合的。
有时候表达式的值可能依赖于子表达式的求值顺序。但是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+2)
又在别处修改它的值(a=1)
是不可取的。有些编译器在遇到这样的表达式时会产生一条类似“operation on 'a' may be undefined”
的警告信息。为了避免出现此类问题,一个好主意就是不在子表达式中使用赋值运算符,而是采用一串分离的赋值表达式。例如,上面的例子可以改写成如下形式:
a = 5;
b = a + 2;
a = 1;
c = b – a;
总而言之,类似这样未定义的,后果是不可预料的语句,我们应该避免使用。
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
没有变化,所以这条语句没有任何作用。
问1:我注意到C语言没有指数运算符。如何求一个数的幂呢?
答:通过重复乘法运算,可以进行较为简单的整数次幂运算(i * i * i
是i
的立方运算)。如果想计算非整数次幂,可以调用pow
函数。
问2:我想把
%
运算符用于浮点数,但程序无法通过编译,该怎么办?
答:%
运算符要求操作数是整数,这种情况下可以试试fmod
函数。
问3:当
/
运算符和%
运算符的操作数是负数时,为什么规则那么复杂?
答:规则其实不像看起来那么复杂。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 都将除法的结果趋零截尾,所以这也被写入这一标准作为唯一允许的结果。
问4:如果C语言有左值,那它也有右值吗?
答:是的,当然。不过在C语言里不叫右值,C语言中的“值”就是“右值”,都是指“表达式的值”。只有左值才可能放在赋值运算符的左侧,否则它就是一个值,或者说右值。当然,C语言不需要“右值”这个概念,C标准也不使用这个概念,这是其他语言,比如C++才使用的概念。
问5:前面提到:如果
v
有副作用,那么v += e
不等价于v = v + e
。可以解释一下吗?
答:计算v += e
只会求一次v
的值,而计算v = v + e
会求两次v
的值。在后一种情况下,对v
求值可能引起的任何副作用也都会出现两次。在下面的例子中,i
只自增一次:
a[i++] += 2;
如果用=
代替+=
,语句变成
a[i++] = a[i++] + 2;
i
的值在别处被修改和使用了,因此上述语句的结果是未定义的。i
的值可能会自增两次,但我们无法确定到底会发生什么。
问6:C语言为什么提供
++
和--
运算符?它们是比其他的自增、自减方法执行得快,还是仅仅更便捷?
答:C语言从Ken Thompson
早期的B语言中继承了++
和--
。Thompson
创造这类运算符是因为他的B语言编译器可以对++i
产生比i = i + 1
更简洁的翻译。这些运算符已经成为C语言根深蒂固的组成部分(事实上,许多最著名的C语言惯用法都依赖于这些运算符)。对于现代编译器而言,使用++
和--
不会使编译后的程序变得更短小或更快,继续普及这些运算符主要是由于它们的简洁和便利。
问7:
++
和--
是否可以处理float
型变量?
答:可以。自增和自减运算也可以用于浮点数,但实际应用中极少采用自增和自减运算符处理float
型变量。
问8:在使用后缀形式的
++
或--
时,何时执行自增或自减操作?
答:这是一个非常好的问题,也是一个非常难回答的问题。C语言标准引入了“序列点”的概念,并且指出“应该在前一个序列点和后一个序列点之间对存储的操作数的值进行更新”。在C语言中有多种不同类型的序列点,表达式语句的末尾是其中一种。在表达式语句的末尾,该语句中的所有自增和自减操作都必须执行完毕,否则不能执行下一条语句。
在后面章节中会遇到的一些运算符(逻辑与、逻辑或、条件和逗号)对序列点也有影响。函数调用也是如此:在函数调用执行之前,所有的实际参数必须全部计算出来。如果实际参数恰巧是含有++
或--
运算符的表达式,那么必须在调用前进行自增或自减操作。
问9:丢掉表达式语句的值意味着什么?
答:根据定义,一个表达式表示一个值。例如,如果i
的值为5
,那么计算i + 1
产生的值为6
。在末尾添加分号,把i+1
变成语句:
i + 1;
执行这条语句时,我们计算出了i + 1
的值,但是我们没有保存这个值(也没有以某种方式使用这个值),因此这个值就丢失了。
本文是作者阅读《C语言程序设计:现代方法(第2版·修订版)》时所做笔记,也是第一篇书籍笔记文章,日后会推出后续章节笔记。感谢各位大佬批评指正,希望对诸位有所帮助,Thank you very much!!