格式化输入和输出其实指的就是C语言标准函数库<stdio.h>中的:
它们是C语言当中使用非常非常频繁的两个函数,所以很重要。
这两个函数的基本使用,比如对int/float数据的打印或者键盘录入,我们在前面的小节已经讲过了,这里不再赘述。
本小节主要讲解一下这两个函数的原理,以及其更强大的功能。
scanf和printf函数看起来一个是从键盘接收数据输入,一个是将数据输出打印到显示器,但计算机内部处理的过程却没有那么简单。
我们通过输入/输出模型来简单了解一下这两个函数大体上的执行原理。
冯诺依曼体系计算机(也叫存储程序控制型计算机),主要包含三大核心组件:CPU、IO设备以及存储器,而存储器当中最重要的则是内存储器,也就是内存。直到今天,硬件设备的发展日新月异,但现代计算机仍没有脱离此体系。
冯诺依曼体系计算机,一个核心问题是CPU、内存以及IO设备三者之间的速度差异从而导致的性能瓶颈,这就是常说的"冯·诺依曼瓶颈”。
具体的说,这个瓶颈指的是:
CPU 的处理速度远远快于内存和 I/O 设备,导致在等待数据处理和传输时,CPU 大部分时间处于空闲等待的状态。这种速度差异造成了显著的性能瓶颈,限制了整个系统的效率。
为了平衡这三者之间的速度鸿沟,一个简单有效的手段是引入缓冲区技术,下面我们简单介绍一下缓冲区技术。
缓冲区本质上是一块临时存储数据的内存区域(一般是在内存中分配的),它在速度较慢的内存和 I/O 设备与速度较快的 CPU 之间起到桥梁的作用。
为了更深入地解释缓冲的工作原理,以printf
和scanf
函数为例,可以更直观地解释其运作机制:
printf
输出数据时,数据并不是立刻写入到输出设备(如屏幕)。它首先被放置在一个stdout缓冲区中,然后在满足特定条件时,数据会被刷新到输出设备。scanf
输入数据时,数据也不是直接从输入设备(如键盘)读取的。它首先被加载到一个stdin缓冲区中,然后 scanf
从这个缓冲区中获取数据。这个过程可以用下图来描述:
图 1. 格式化输入输出-缓存模型
那么添加这样的一个缓冲区有什么好处呢?
使用缓冲区的好处显而易见——提高IO性能。
缓冲区是如何提高IO性能的?
printf函数和scanf函数:
这些都是非常典型的I/O操作过程。
我们都知道,I/O的过程效率很低。除了硬件性能本身的差异外,I/O操作的复杂性也是非常重要因素。每次进行I/O操作都会带来一些固定的开销,比如:
总之,如果每输入或输出一个字符都要进行一次完整的I/O操作,那么这些固定的开销就会迅速积累,导致性能显著下降。
硬件层面的效率低下,我们没有办法通过软件层面的优化去解决。但对于这些大量的固定开销,我们可以通过缓冲区来进行效率优化。
缓冲区的主要目的是暂时存储数据,然后在适当的时机一次性进行大量的I/O操作。这样,多个小的I/O请求可以被组合成一个大的请求,有效地分摊了固定开销,并显著提高了总体性能。
拿上述两个函数,具体来说:
stdin
的缓冲区中。当满足某个触发条件后,程序才会从缓冲区读取并处理这些字符,从而减少了IO交互次数。stdout
的缓冲区。当满足某个触发条件后,这些内容会一次性写入并显示到屏幕,降低了与显示设备的交互频率。如果你还不理解,就想象一次I/O操作就是搬运工搬运货物的过程,货物总量是一定的,搬一趟的时间也是差不多的。那么当然是一次性搬得尽量多,搬运的次数尽量少,总效率越高。
不使用缓冲区的I/O操作就像搬运工每次只能手提一个货物,频繁往返。而使用缓冲区,则好比搬运工使用了一个小推车,可以一次性搬运多个货物,大大提高了效率。
从上述内容中,我们可以明确地看到缓冲区的一个显著特点:当满足特定的条件时,程序会开始对缓冲区内的数据执行输入或输出操作。
这种**“满足条件即触发数据传输”的行为,被我们称为“缓冲区的自动刷新”**机制。
基于这种自动刷新的触发条件的不同,我们可以将常见缓冲区划分为以下三种类型:
补充和注意事项:
关于缓冲区,有以下几点需要特别注意:
printf
函数的输出通常也会立即显示在控制台上。这种行为是为了帮助程序员更有效地调试程序,即时看到他们的输出,而不需要固定等待缓冲区刷新条件。至此,我们已经对输入输出的基本概念有了全面的了解。接下来,我们将深入探讨具体的函数如何使用。
当涉及到函数调用时,虽然查阅文档是重要的学习步骤,但真正的关键在于实践:亲自编写和执行代码。
printf函数的核心作用是将各种类型的数据转换为字符形式并输出到stdout缓冲区中。
从实际效果看,printf函数会展示格式字符串的内容,并在指定的位置插入对应的值。
调用printf函数时,首要参数是格式字符串。紧随其后的参数表达式则表示要插入到该字符串中的值。调用形式如下:
printf(格式字符串, 表达式1, 表达式2, ...);
一个我们已经使用过的代码示例如下:
代码块 1. printf函数-打印int和float类型
int i, j;
float x, y;
i = 10;
j = 20;
x = 43.2892f;
y = 5527.0f;
printf("i = %d, j = %d, x = %f, y = %f\n", i, j, x, y);
格式字符串包含两个主要部分:
理解转换说明的含义和用法是掌握printf函数的关键。
转换说明在printf
函数中起到了关键的角色,允许开发者对输出格式进行精细的控制。它主要有以下几个作用:
系统的讲,转换说明的组成公式如下:
1
%[标志][字段宽度][.精度][长度]说明符
注意,"%"和"说明符"是必不可少的,其余部分则是可选的!可选的部分用[]括起来了!
下面,我们一步步详细地讲解每个部分:
a-f
)。A-F
)。通过结合这些组件,你可以精确地控制printf
的输出格式。但是请不要尝试死记硬背,要在不断使用的过程中,逐渐理解记忆。当遇到不会写的格式或者忘记时,再及时查表即可。
"%"的使用
在转换说明中,有一个非常特殊的字符——“%”。百分号用于转换说明的开始,那么如果我就希望打印一个百分号咋办?
很简单,用"%%“来表示一个”%"。
printf函数将数据写入stdout的行缓冲区,但要将这些数据真正展示到外部设备(如屏幕),则需依靠stdout的自动刷新机制。
为了增加输出的实时性和可预测性,一个常见策略是在输出字符串的末尾添加换行符"\n",这样可以立即触发缓冲区的刷新。这确保了待显示的信息能够迅速呈现,不会因其他因素延迟。
建议:
在不影响程序的逻辑的前提下,调用printf函数的格式字符串应当总是以换行符"\n"结尾。
小练习当中,涉及的都是目前已经学习过的,并且比较常用的转换说明。
思考并回答以下代码的输出结果:
代码块 2. 转换说明-练习题1
printf("|%4f|\n", 3.14159f);
printf("|%10f|\n", 3.14159f);
printf("|%.4f|\n", 3.14159f);
printf("|%.7f|\n", 3.14159f);
printf("|%4.1f|\n", 3.14159f);
printf("|%04.1f|\n", 3.14159f);
printf("|%-4.1f|\n", 3.14159f);
代码块 3. 转换说明-练习题2
float value = 0.1f;
printf("%.10f\n", value);
int i = 40;
float x = 839.21f;
printf("|%d|%5d|%-5d|%5.3d|\n", i, i, i, i);
printf("|%f|%10f|%10.2f|%-10.2f|\n", x, x, x, x);
请按照下列要求,编写代码:
参考代码:
代码块 4. 转换说明-练习题3参考代码
#include <stdio.h>
int main() {
int chinese, math, english;
float average;
printf("请输入语文成绩:");
scanf("%d", &chinese);
printf("请输入数学成绩:");
scanf("%d", &math);
printf("请输入英语成绩:");
scanf("%d", &english);
average = (chinese + math + english) / 3.0; // 使用 3.0 以确保结果是浮点数
printf("平均成绩为:%.2f\n", average);
return 0;
}
代码块 5. 转换说明-练习题4参考代码
#include <stdio.h>
int main() {
float input, percentage;
printf("请输入一个(0, 1)范围的浮点数:");
scanf("%f", &input);
if (input <= 0 || input >= 1) {
printf("输入值超出范围!\n");
return 1; // 返回一个错误代码
}
percentage = input * 100; // 转换为百分比
printf("转化后的百分比为:%.1f%%\n", percentage);
return 0;
}
在早期的 C 语言标准(如 C89/C90)中,所有的局部变量必须在函数或代码块的开始处声明,这是强制的语法要求。
这种强制的语法设定,导致你会看到诸如下列代码:
代码块 6. 把局部变量声明放在函数开头-演示代码
int main(void){
int i, sum = 0;
for(i = 1; i <= 100; i++){
sum += i;
}
printf("1 + 2 + ... + 100 = %d", sum);
return 0;
}
但从C99标准开始,这个限制被放宽。在C99和之后的标准中,你可以在任何地方声明局部变量,只要在使用它们之间声明即可。
很显然这样,程序员就可以在更接近局部变量的实际使用地方来声明它,从而提高代码的可读性和可维护性。
综上,我们给出以下建议:
scanf函数的核心作用是从stdin缓冲区中读取字符形式的数据,并将其转换为特定类型的数据。
从实际效果看,scanf函数会根据格式字符串读取输入的内容,并将这些内容赋值给指定的变量。
调用scanf函数时,首要参数也是格式字符串,紧随其后的参数是变量的地址,表示将读取到的值存放在哪个位置。调用形式如下:
1
scanf(格式字符串, &变量1, &变量2, ...);
我们以往已经使用过scanf函数了,一个示例代码如下:
代码块 7. scanf函数-读取int和float类型
int i;
float x;
printf("输入整数: ");
scanf("%d", &i);
printf("输入浮点数:");
scanf("%f", &x);
printf("您输入的数据是: i = %d, x = %f", i, x);
scanf函数的格式字符串中可能包含:
值得注意的是,scanf函数在调用时填入的变量前面要加符号"&",它是取地址运算符,意思是告诉scanf函数将数据存储到某个地址。它一般是必须的,但有些情况下可以省略。
scanf函数使用转换说明来解析和读取输入,这为开发者提供了对输入数据格式的精细控制。
系统地讲,scanf函数的转换说明的组成公式如下:
%[*][宽度][长度]说明符
其中,"%"和"说明符"是必不可少的,而其他部分则是可选的。我们用[]括起来表示这些可选的部分。
接下来,我们逐一解释每个组成部分:
i
和printf的是不同的。
和printf函数一样,当你在使用scanf函数时,建议不要死记硬背转换说明,而是在实践中逐渐熟悉并查阅文档或其他资料进行验证和参考。
scanf函数本质上是一个**“模式匹配"函数,试图把"stdin缓冲区”**中的字符与格式字符串匹配。
scanf函数会从左到右依次匹配格式字符串中的每一项:
除此之外,scanf函数的转换说明符大都默认忽略前置的空白字符,这样的设计让输入对用户更好友好,比如:
练习,下列代码的执行结果是什么?
代码块 8. scanf函数-练习题1
int i, j;
float x, y;
scanf("%d%d%f%f", &i, &j, &x, &y);
分别键盘录入以下数据:
100 200 0.1 0.2
1-20.3-4.0e3
100a2000.10.2
1002000.10.2
结果是什么呢?为什么?
注意事项:
scanf 函数用 %c 格式化字符串来读取单个字符时,并不会跳过空白字符,%c 会读取输入的下一个字符,无论它是什么,包括空白字符。
所以在录入字符时,尤其是一行录入多个数据且包含输入字符时,一定要在转换说明前面留出一个空格,以匹配可能的空格:
代码块 9. %c转换说明使用注意事项
char ch;
int num;
printf("请输入一个数字以及一个字符: ");
scanf("%d %c", &num, &ch); // 注意 %c 前的空格
printf("你输入的数字是: %d\n", num);
printf("你输入的字符是: %c\n", ch);
上述代码运行,键盘录入:
100 a
程序打印结果:
你输入的数字是: 100
你输入的字符是: a
scanf函数的格式字符串串中也可以包含普通字符("%"之前的部分是普通字符),和printf函数不同的是,scanf函数的普通字符也用来表示匹配规则。
例如:
因此,scanf中的普通字符不仅仅是装饰或分隔,它们也参与到输入数据的匹配中,确保数据的格式正确。
代码块 10. scanf函数-练习题2
int i, j;
scanf("%d/%d", &i, &j);
分别键盘录入以下数据:
5/ 96
5 / 96
结果是什么?
其它不变,把转换说明改完"%d /%d",再次录入数据,有区别吗?为什么?
虽然scanf函数调用和printf函数调用看起来很相似,但这两个函数之间有很大的差异!
一个常见的错误是:调用printf
函数时,在变量的前面加 &。
1
printf("%d, %d\n", &i, &j); /*** WRONG ***/
scanf
函数在寻找数据项时,通常会跳过前面的空白字符。所以除了转换说明,格式串通常不包含其他字符。
一些常见混淆printf函数,导致的错误是:
代码块 11. scanf函数调用常见-错误写法
scanf("%d, %d", &a, &b); // 这样写输入的数据必须是 "10, 20"格式的
scanf("%d\n", &a); // 错误的添加换行
写一个程序,实现分数相加。用户以分子/分母的形式输入分数,程序打印相加后的结果。如:
图 2. 综合练习题-图
拓展:如何将结果化为最简分数?
注意:当 scanf 函数遇到一个不属于当前项的字符时,它不会读取该字符。在下一次读取输入时,才会读取该字符。
参考代码如下:
int numerator1, denominator1; // 第一个分数的分子和分母
printf("请输入第一个分数:");
scanf("%d/%d", &numerator1, &denominator1);
int numerator2, denominator2; // 第二个分数的分子和分母
printf("请输入第二个分数:");
scanf("%d/%d", &numerator2, &denominator2);
// 分子错位相乘相加
int result_num = numerator1 * denominator2 + numerator2 * denominator1;
int result_denom = denominator1 * denominator2;
printf("sum = %d/%d", result_num, result_denom);
注意,变量命名要见名知意,不要乱用a、b、c等无意义字符。除此外为代码增加必要的注释也是一个非常好的编程习惯。