登录  | 立即注册

游客您好!登录后享受更多精彩

扫一扫,访问微社区

QQ登录

只需一步,快速开始

开启左侧

[寒假笔记] 关于C语言重定向和缓冲区的一些学习

[复制链接]
发表于 2018-1-28 11:46:32 | 显示全部楼层 |阅读模式
学习笔记
学习科目: C语言
学习安排: 借助一些c语言相关书籍对知识进行一些查缺补漏和整理
开始时间: 2018-01-25
结束时间: 2018-03-02
本帖最后由 ysxx1013 于 2018-3-2 23:58 编辑

在之前的电子设计竞赛中,我写出了一个简易的学生成绩管理系统,但迫于时间紧迫与个人能力有限,还有很多程序中的问题没有解决。
在这里,我希望通过学习一些相关的零碎知识,最终完善当初的程序
本篇主要是对C语言中重定向与缓冲区相关的问题做一些记录

=============================开发环境=============================
操作系统:Microsoft Windows 10 专业版 10.0.16299
CPU:Intel(R) Core(TM) i3-4130 CPU @ 3.40GHz
内存:金士顿 DDR3 1600MHz 4GB*2
显卡:NVIDIA GeForce GT 1030
所用IDE及工具链:Code::Blocks 16.01+MinGW-w64
==================================================================
(本文中大部分例程来自C Primer Plus中相关例程)
学习C语言的同学,应该对getchar()与putchar()函数都不陌生
这里通过一个简单的例程来复习一下这两个函数的基本用法
[mw_shl_code=c,true]/*p217 echo.c*/
#include <stdio.h>
int main(void)
{
    char ch;

    while ((ch = getchar()) != '#')
        putchar(ch);

    return 0;
}
[/mw_shl_code]
该程序获取从键盘输入的字符,并把这些字符发送到屏幕上。程序使用while循环,当读到#字符时停止。
2018-01-27.png
程序运行结果

但同样的程序在一些古老的编译器/系统中可能会出现问题,比如说出现下图的情况
2018-01-27 (1).png
老式系统中该程序可能的运行结果

从《C Primer Plus》书中可知:
像这样会先用户输入的字符后立即重复打印该字符是属于无缓冲(或直接)输入。对于该例,大部分系统在用户按下Enter键之前不会重复打印刚输入的字符,这种输入形式属于缓冲输入。用户输入的字符被收集并储存在一个被称为缓冲区(buffer)的临时存储区,按下Enter键后,程序才可使用用户输入的字符。

ANSI C和之后的C标准都注明:C语言中的输入是缓冲的,但早期的未规范的C语言程序中有可能出现如上例所示的无缓冲输入
在现行的C标准中,没有提供调用无缓冲输入的标准方式。
也就是说,在我们现在编写的符合规范的C语言程序,都是要使用缓冲区的。
为了更好的理解缓冲区的概念,下面我们看一个简单的例程:
[mw_shl_code=c,true]#include <stdio.h>
//例程1
int main(void)
{
    int a[5];
    for(int i=0;i<5;i++)
        scanf("%d",&a);
    for(int i=0;i<5;i++)
        printf("%d ",a);
    return 0;
}
[/mw_shl_code]
让我们看一下这个程序的一个简单运行范例:
2018-01-27 (2).png
我实际输入的是:123[空格]123[空格]423[空格]234[空格]4[回车]
那么,在我输入回车之前,“123[空格]123[空格]423[空格]234[空格]4”这一段数据就是保存在缓冲区中
然后再由scanf函数一个一个将其中的数据调用出来。
不过,我们要注意的是,实际上,回车所输入的换行符“\n”也是会被输入缓冲区的。
在某些应用当中需要特别注意,这里限于篇幅不深入讨论

讲了这么多,我们再回头来看看本篇文章中的第一个例程:
[mw_shl_code=c,true]/*p217 echo.c*/
#include <stdio.h>
int main(void)
{
    char ch;

    while ((ch = getchar()) != '#')
        putchar(ch);

    return 0;
}
[/mw_shl_code]
这个程序虽然完成了“从键盘输入字符再显示出来”的任务,但很明显完成的并不完美。
特别是,在当我们输入'#'字符后,后面的内容便不再被程序录入。
那么,我们该如何解决这样的问题呢?让我们来看看C语言给出的解决方案:
首先,让我们了解一下C语言中文件、流和键盘输入的关系
(以下摘自《C Primer Plus》)
C是一门强大、灵活的语言,有许多用于打开、读取、写入和关闭文件的库函数。从较低层面上,C可以使用主机操作系统的基本文件工具直接处理文件,这些直接调用操作系统的函数被称为底层I/O。由于计算机系统各不相同,所以不可能为普通的底层i/o函数创建标准库,ANSI C也不打算这样做。然而下哦那个较高层面上,C还可以通过标准I/O包来处理文件。这设计创建用于处理文件的标准模型和一套标准I/O函数。在这一层面上,具体的C实现负责处理不同系统的差异,以便用户使用统一的界面。
上面讨论的差异指的是什么?例如,不同的系统储存文件的方式不同。有些系统把文件的内容储存在一处,而文件相关的信息储存在另一处;有些系统在文件中创建一份文件描述。在处理文件方面,有些系统使用单个换行符标记行末尾,而其他系统可能使用回车符和换行符的组合来表示行末尾。有些系统用最小子节来衡量文件的大小,有些系统则以字节块的大小来衡量。
如果使用标准I/O包,就不用考虑这些差异。因此,可以用if (ch == '\n')检查换行符。即使系统实际用的是回车符和换行符的组合来标记行末尾,I/O函数会在两种表示法之间相互转换。
从概念上看,C程序处理的是流而不是直接处理文件。流是一个实际输入或输出映射的理想化数据流。这意味着不同属性和不同种类的输入,由属性更统一的流来表示。于是,打开文件的过程就是把流与文件相关联,而且读写都通过流来完成。

我们平时在C语言中的学习中,使用的最多的大概就是表示键盘输入的stdin流和屏幕输出的stdout流了
既然我们的键盘输入和屏幕输出在C语言内部的“地位”与指代文件流一样,都是“流”
那么,程序读文件时要能检测文件的末尾才知道应在何处停止。因此,C的输入函数内置了文件结尾检测器
既然键盘输入和文件输入是等价的,那么我们能不能在C语言中调用文件结尾检测器来结束键盘输入,
解决例程中出现的问题呢?
(未完待续)

以上部分编辑于2018-1-28 11:46
以下部分补充于2018-2-1
万分抱歉,由于近期事务繁忙,笔记未能及时更新
这一次,就当作是“番外加更”,同大家再讲讲关于C语言缓冲区的一些问题
之所以想在回归更新笔记的“主线”之前先更新这样的一篇“番外”
主要是在一个大学生编程相关的qq群中,有一位同学问了我他/她的程序有什么问题
我看了看这个程序,发现这是个讲解缓冲区的不错的例子
于是在争取了原来那位同学的许可下,我得以将这个程序作为例程展现给大家
这是那位同学一开始的程序:
[mw_shl_code=c,true]#include <stdio.h>
struct Lovers
{
        char sex;
        double height;
};
int main(void)
{
        int N, i;
        struct Lovers s[10];
        scanf("%d", &N);
        for (i = 0; i < N; i++)
        {
                scanf("%c %lf", &s.sex, &s.height);
   
     }
        for (i = 0; i < N; i++)
        {
                if (s.sex == 'M')
                        printf("%.2f", s.height / 1.09);
                if (s.sex == 'F')
                        printf("%.2f", s.height * 1.09);
                if (i != N)
                        printf("\n");
        }
        return 0;
}[/mw_shl_code]
从代码中可以看出这是一个求最佳伴侣身高的程序,这名同学的意愿我们也可以分析的很清楚
从一开始的scanf函数中输入人数,然后根据人数进行循环
在每一次循环中,用scanf函数输入每个人的性别和身高信息,最后再使用printf函数和循环输出符合每个人的最佳伴侣身高信息
可是,这个程序的运行结果却并不能完美符合这位同学的心意
程序运行结果如下图:
2018-02-01.png
我们可以推测出来,这位同学是想输入两个人的数据,但是在他/她刚刚输入完第一个人的数据并按下回车后
程序却输出了意料之外的结果
出现这种状况的原因是什么呢?让我们打开IDE的调试模式一探究竟
(注意,此处最好使IDE处于“调试(debug)”状态)
2018-02-01 (3).png
上图中为Code::Blocks17.12中与Microsoft Visual Studio 2017中切换调试(debug)与发行(release)模式的地点
(可能会与你的电脑稍有差异)

我们在程序的第十六行设置断点,以调试模式执行程序
X8SO~ZDDY@6D4Z5ZJVALSGJ.png
可以看到我们的程序运行停止在了断点处
此时我们可以查看内存中各变量具体的值
我们可以惊讶的发现,结构体数组s[]当中s[0]的数据与我们所预计的并不一样
sex中所储存的单个字符的值竟然是'\n',而height中所储存的身高值更是看起来就像是内存中未初始化的数据
这是为什么呢?
原来,在我们输入“2[回车]”的时候
不仅'2'进入了缓冲区,“回车”所代表的'\n'同样也进入了缓冲区
在我们平时使用scanf函数时常使用的指定转换符%d会跳过空格和换行符
但%c可不是这个样子的
程序12到15行的for循环中的scanf函数第一次运行时
scanf函数中的%c读取到一个换行符,并将其存入s[0].sex中,而由于scanf函数读取到了换行符,函数运行结束
因此实际上s[0].height的值仍然是内存中未初始化的初值
换个角度看,for循环中第一次运行的scanf在我们认为其运行之前就已经运行结束了
实际上我们感觉到运行的只有i=1时的scanf函数
那么,如何避免这个问题呢?
在这里放上一个比较简单的解决方案
[mw_shl_code=c,true]#include <stdio.h>
struct Lovers
{
        char sex;
        double height;
};
int main(void)
{
        int N, i;
        char ch;
        struct Lovers s[10];
        scanf("%d", &N);
        ch = getchar();
        for (i = 0; i < N; i++)
        {
                scanf("%c %lf", &s.sex, &s.height);
                ch = getchar();
        }
        for (i = 0; i < N; i++)
        {
                if (s.sex == 'M')
                        printf("%.2f", s.height / 1.09);
                if (s.sex == 'F')
                        printf("%.2f", s.height * 1.09);
                if (i != N)
                        printf("\n");
        }
        return 0;
}[/mw_shl_code]大家可以结合我上面对缓冲区的一部分讲解,理解一下这个程序的原理


以上部分最后编辑于2018-2-1 12:36
以下部分补充于2018-2-8
接着上一次断掉的话题,我们该如何在C语言中调用文件结尾检测器来结束键盘输入,
解决例程中出现的问题呢?
在《C Primer Plus》中,对检测文件末尾有着这样的叙述:
无论操作系统实际使用何种方法检测文件结尾,在c语言中,用getchar()读取文件检测到文件结尾时将返回一个特殊的值,即EOF(end of file的缩写)。scanf()函数检测到文件结尾时也返回EOF。通常,EOF定义在stdio.h文件中

对于这一点的应用,我们可以看下面的例程:
[mw_shl_code=c,true]/* P221 echo_eof.c -- 重复输入,直到文件结尾*/
#include <stdio.h>
int main(void)
{
        int ch;

        while ((ch = getchar()) != EOF)
                putchar(ch);

        return 0;
}[/mw_shl_code]
其运行结果如下:
捕获3.PNG
在windows的命令提示符界面中,要用键盘输入文件结束标记,需要同时按下Ctrl+Z
此时在屏幕上所显示的就是^Z,但实际上在程序中这相当于输入了'EOF'
于是程序停止
通过类似这样的方法,我们就可以做到在不影响正常输入的情况下,通过模拟文件结束标记来进行输入的结束

那么,我们现在已经知道如何通过模拟文件结束标记来结束程序了
但当我们想要让现有的程序将结果输出到某个文件中
或是从某个文件中读取输入数据
我们又该怎么办呢
这时,就是操作系统所带有的重定向功能派上用场的时候了
那么,重定向功能又该如何使用呢?
把上一节的程序作为例子,我们来测试一下
首先,我们在上节例程的文件夹下新建一个名为words的文本文档
捕获4.PNG
并在其中输入如下图中内容保存
捕获5.PNG
之后,我们通过命令提示符方式调用程序,并使用重定向符'<'来指定输入为words.txt文件
程序运行结果如下:
捕获6.PNG
可以看到,程序把words.txt中我们所输入的一段话当做了stdin流,即标准输入流中的输入
同样的方法,我们也可以用重定向符'>'来指定一个文件用来输出
例如:
捕获7.PNG
程序运行完成之后,在其文件夹下生成了output.txt文件
捕获9.PNG
其内容为
捕获8.PNG
正是我们刚才所输入的内容
好懒~~不想说~~~
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表