C的输入输出

引入一个概念,对于计算机来说,外来数据都是输入,经过计算机处理的结果并进行显示的就是输出。在linux里面,一切都是文件,就连输入输出,都可以划归到"文件"一类,而为了管理这些文件,有个概念叫文件描述符,它们都是整数,对应着不同的文件。文件描述符0表示标准输入,文件描述符1表示标准输出,文件描述符2表示标准错误输出;标准输出和标准错误输出都对应硬件平台的屏幕,而标准输入通常对应键盘,这是最早期最简单的概念。伴随着计算机功能的强大,能处理的东西越来越多,这个概念也慢慢变得复杂模糊,以后会不会变真不清楚,就像现在的输入不单单是简单的键盘输入,就windows电脑而言,鼠标的移动、左右键的单击、u盘的插入其实都能算是输入了,而输出就是前者被电脑处理后的反应了,概念是基石,不是狭窄的通道,不要让它们限制了自我。

一、标准输入和标准输出

1.1 最早接触的scanf和printf

C标准库中针对标准输入输出的,最早接触的基本就这两个,都使用的格式字符串和参数列表且工作原理相同的两个函数。函数原型如下:

int scanf(char const *format,...);
int printf(char const *format,...);

格式化字符串中,经常遇到的就是格式转换符,统一一下:

转换符 说明(因为可以用于输入也可用于输出)
%d 十进制整数
%u 无符号整数
%hd 十进制短整数
%ld 十进制长整型整数
%x 无符号十六进制整数
%o 无符号八进制整数
副词# 添加在八进制或者十六进制转换符中,可以输出进制前缀,如#o、#x
------ ----------我是分割线-----------------------------
%c 代表单个字符
%f 浮点数,十进制计数
%e 浮点数,科学计数法,小写e
%E 浮点数,科学技术法,大写E
%g 自动调整精度,根据需要判断输出十进制计数还是科学计数%e
%G 自动调整精度,根据需要判断输出十进制计数还是科学计数%E
%a 十六进制浮点数,科学计数法,小写a(如果系统支持)
%A 十六进制浮点数,科学计数法,大写A(如果系统支持)
%Lf long double型,十进制计数,要用科学计数法表示也要添加L
------ -------------又是分割线------------------------------
%s 字符串
%p 指针
%% 一个百分号

上面的转换符既可用于printf输出也可用于scanf接收,但对于连续输入,还是要注意一下,不然很容易就出bug。(副词#一项不行,只可用于格式化输出)

根据目前我能找到的信息,C好像是不支持有符号整数的八进制和十六进制输出的,准确来说是不支持负数的八进制和十六进制,如果强行输出,得到的就是一个第一数位为1的二进制数转换来的非常大的一个数值,因为它以无符号来读取这个数并进行进制转换,用于输入也是同理。

  • printf的转换说明修饰符

上面的表格有提到一个副词#,这个其实就是转换说明修饰符之一,这些插入在%和转换符之间的符号,对转换加以修饰说明,所以称为转换说明修饰符,而在格式化输入里面不使用纯粹是为了降低复杂度,不然输入一个八进制整数,再加一个前缀’0’,本来就是一连串字符了,再来一首这个,简直是难为胖虎。

修饰符 说明
五种标记 #,提示输出前缀
-,输出左对齐(默认输出是右对齐的)
+,确定正负添加对应±
空格,对应正负输出空格或负号
0,确定了输出宽度的情况下,前面有空缺就用前导0补上
数字 最小字段宽度
在设置宽度不能容纳输出的数字或字符串时,系统自动调整宽度
.数字 突出顿号’.',设置输出精度
%e转%f,表示小数点右边数字位数
对于%s,表示输出字符串中字符数
对于整型则是宽度,有必要的时候情况下用前导0补充
%.f和%.0f等同,精度为0的意思
顿号前数字表示宽度,即一共输出多少位数字,但如果和顿号后面的精度产生冲突,以精度为准
h 常见于配合整型转换说明符,表示输出短整型
hh 输出unsigned char类型,就是单字节无符号整型
ll 常见于配合整型转换说明符,输出long long整型
L 配合浮点型转换说明符,输出long double型数值
t 配合整型转换说明符,输出ptrdiff_t类型值,指两指针差值的类型
z 配合整型转换说明符,输出size_t类型值,sizeof返回类型值,专用于检查存储字节大小

上面有个有意思的转换说明修饰符–hh,为了配合输出unsigned char类型整数,实际上,char类型只是用来存储字符,它在技术本质上还是整数类型,所以对应的也就有无符号char类型了,而hh就是为这个类型准备的。其实scanf的转换说明修饰符和printf的只有些许不同,比如(#、0、.数字)不适用,而scanf的*不适用于printf。

转换其实并没有改变本质,在计算机存储中,它们还是一个个二进制数,这只是用不同的规则来翻译这段二进制数的表征意义

scanf在一板一眼的单个输入输出中很正常,当但你连续输入时往往就会有问题。

  • 类型要匹配,输入时需要针对转换说明符进行输入,这个比较简单,不表
  • 在格式字符串中,允许普通字符的存在,但空格以外的普通字符都必须与输入字符匹配,比如你加了个逗号在其中,你的输入也要以逗号为分隔符(有意思的是,如果你加了空格,你可以以换行符作为分割,缩进也行,其他空白符我没试过),所以一般为了不给自己找麻烦都是不添加太多花样字符
  • scanf是在读取到空白的时候停止输入,所以如果你接受的输入时字符串往往只能接收到第一个单词而不是你输入的整个句子
  • 读取不同类型数据的scanf使用,在读取数字需求时,scanf遇到输入缓存里面的空白(通常是换行符)就结束接收,因此这个空白就会停留在输入缓存里,而下一次的scanf是读取的字符数据,往往就会给字符串赋以换行符,所以这种情况往往要为了处理这个残留的换行符而添加getchar,另外也可以在scanf开始接收字符数据的时候,在格式化字符串开头添加换行符"\n"

总结一下,scanf和printf的工作都是转换–数值和文本之间的相互转换:从键盘输入的是文本,也就是字符串,输出到屏幕的也是字符串,而计算机本身存储的是数值。

1.2 其他输入输出函数

  • 简单的getchar和putchar

字符串是单字节字符类型的连续序列,有字符串处理函数就有字符处理函数,getchar就是从输入队列里面获取一个字符,而putchar就是输出一个字符,然后换行(因为printf格式化输出都是换行符控制换行,这个是自带换行,这是不同)。使用如下:

char ch;
ch = getchar();   //输入字符a
putchar(ch);      //'a'

//等同于以下语句
scanf("%c", &ch);
printf("%c\n", ch);

这两个函数非常简单,常见于逐个读取输入队列来进行处理的场景,可以配合循环语句来实现printf和scanf的格式化字符串的识别和分别处理场景。实际上,它们并不是真正的函数,而是一个个宏。

关于缓冲区
对于交互式程序,当用户进行输入后程序马上做出反应进行打印输出,这种就是无缓冲输入,我们常见的方式是进行一连串输入后按下enter键后才会进行处理输出,这种就是缓冲输入,将输入作为块进行传输会节约时间,另外有缓冲的情况下可以对输入进行修改等等。缓冲输入好处多多,但无缓冲输入也不是没有好处,针对需要即时反应的系统程序来说,这种输入就显得非常重要,所以只是应用场景有区别而已。
关于缓冲输入,有两种:完全缓冲和行缓冲,前者应用场景是文件输入,后者应用于日常与用户交互。

  • 处理行数据的gets()和puts()

如上面所言,日常应用于交互的输入输出函数都是处理行缓冲的,而scanf遇到空白就停止处理的行为不太符合实际,所以这里是针对一行输入输出数据的两个函数。原型如下:

char *gets(char *s)
int puts(const char *s)

前者从标准输入中读取一行数据,成功就返回存储了该数据的地址,失败则返回NULL;后者则是输出字符串到标准输出,直到遇到结束符,然后追加换行符,成功就返回非0字符串长度(包括\0),失败则返回EOF。

  • 针对字符串的格式化输入输出

两函数原型如下:

int sscanf(const char *str, const char *format, ...)
int sprintf(char *str, const char *format, ...)

sscanf是从str指示字符串中读取格式化输入,分别给后面的变量列表赋值,成功就返回成功适配和赋值的个数,到达尾部或者失败则返回EOF;sprintf发送格式化输出到str指示地址。

二、文件输入输出

文件,存储器中存储数据的区域,一个文件的使用通常为:打开文件(创建),然后输入输出等处理,最后关闭文件。相对的,C标准库中也有着打开和关闭文件、写入和读取文件等函数,这些函数基本都是直接调用操作系统的函数来进行处理,这种函数被称为底层IO。

对于文件,不同的操作系统用不同的办法来检测文件末尾,linux的行末尾是一个简单的换行符,windows里面的行末尾是换行符和回车符的组合,那文件末尾呢?由于使用的是C语言,所以C里面有特定的检测,来平复操作系统的不同所造成的差异(GNU和vs的差异不算其内,这是两种不同实现了)。在C中,统一称文件末尾是一个特殊的值:EOF,这是一个宏,在stdio.h中声明的宏,对于用户来说:

#define EOF (-1)

值为-1是为了不与标准字符集对应,可以作为一个特殊独立的值存在,从而作为文件末尾的标记。但有些系统并不一定是这个值,这也没关系,用的是这个宏就行。比如我们的判断这么用:

(ch = getchar()) != EOF

两种文件和C中的两种文本模式

在计算机中,所有文件,都是二进制数存储,如果其二进制数编码的字符表示文本,这种文件就是文本文件;如果二进制数表示机器语言代码或者数值数据、图片乃至音乐编码,这种文件就是二进制文件

针对如何处理文件,不同系统有不同的规范,在C中,进行了标准的统一,只为用户提供两种访问文件的方式:
二进制模式,程序访问到的内容就是原来的样子,不对文件中的\r或者\r\n进行映射
文本模式,程序访问到的内容是进行修改的,统一把\r换成\n,\r\n换成\n,而这个会在程序对文本内容进行改动后进行还原。

2.1 文件使用

  • 打开/关闭文件

关于文件的打开关闭函数原型如下:

FILE *fopen(const char *filename, const char *mode)
int fclose(FILE *f)

前者为文件打开函数,后者为文件关闭函数,前者接收文件名的字符串参数和打开模式参数,根据不同模式来确定该文件能有什么操作。

打开模式 说明
r 读模式
w 写模式,文件不存在就新建一个文件,文件已存在就覆盖
a 写模式打开文件,现有文件末尾添加输入,文件不存在就新建一个文件
r+ 读写模式打开已有文件,没有就返回NULL
w+ 读写模式打开,文件存在就覆盖,文件不存在就新建
a+ 读写模式打开文件,文件已存在就在文末进行写入,文件不存在就新建
b 可以和上面任意模式搭配使用,不过是以二进制模式打开文件
x 和上面任意模式搭配,文件存在就独占,不存在就打开失败

fopen调用成功,返回文件指针,打开失败,返回NULL;
fclose使用简单,直接调用即可,一般是在文件处理完毕,不使用文件的情况下调用,已经close的文件在调用前需要open。

  • fprintf写入文件,fscanf读取

和printf以及scanf一样,fprintf和fscanf两个函数也负责输出和输入,不过对象不一样,函数原型如下:

int fprintf(FILE *stream, const char *format, ...);
int fscanf(FILE *stream, const char *, ...);
void rewind(FILE *stream);

fprintf和fscanf函数的使用比较简单,和printf以及scanf一致,只是需要多传一个参数(fopen成功创建的文件指针),另外,上面添加了一个rewind函数,这个是一个"反悔"函数,它能够回到你文件指针一开始操作的地方,就是你刚开始fopen打开文件的时候,但,它只是回到那个位置,你之前的写入,是不会更改的,反悔了但没完全反悔?

另外,但你给传入fprintf的文件指针是表示标准输出的stdout,它也能当做printf一样用,输出到屏幕;传入fscanf的文件指针为表示标准输入的stdin的时候,也能当做scanf使用。

  • fputs和fgets

fputs和fgets与gets以及puts稍有不同,不仅是参数,函数原型如下:

int fputs(const char *s, FILE *stream)
char* fgets(char *s, int n, FILE *stream)

fputs可以向文件指针stream指定文件输出字符串s,和puts不同,fputs并不会附赠一个\n;fgets从文件指针stream指定文件读取n-1长度的字符并存到s数组,并会自动添加\0,如果s数组不足以存下n-1个字符,函数失败,所以一般这个参数用sizeof(s)即可。

  • fseek和ftell

这是两个非常有用的函数,但应用场景让我很迷,函数原型如下:

int fseek(FILE * f, long Offset, int Origin)
long ftell(FILE *f)

fseek函数能够在文件指针f指定文件,在origin指定位置进行或正向或反向的偏移。origin指示位置的常量如下:

模式 位置
SEEK_SET 文件起始处
SEEK_CUR 当前位置
SEEK_END 文件末尾

在指定了origin后,offset为正数,表示正向偏移对应字节数,负数表示反向偏移对应字节数(offset为长整型,一般是10L、-5L的值,表示正向移动10个字节,反向移动5个字节)。函数正确执行返回0,否则返回-1。

ftell则是返回文件中的当前操作位置,当然是相对的,相对文件开头的位置。返回的值是一个长整型,表示当前距离文件开头多少个字节。刚打开文件没有任何操作的情况下,默认位置是文件起始处。

函数使用倒是明了,但具体用在什么时候实在不清楚。

三、流的输入输出

实际上,C程序处理的是流(一个实际输入输出的映射),而不是直接文件,所以很多函数原型中关于文件指针,在很多地方也可解读成stream,比如上面的fprintf和fscanf,当传入的文件指针是为标准输入和标准输出时,它们也可等同于printf和scanf,这就是一种统一,也可以说是C针对系统的一个抽象和封装,呈现给用户一种样子。

流的处理其实在c++中相对更成型,c中因为历史原因并没有什么完整的一套,暂时我找到的资料是这样的。

猜你喜欢

转载自blog.csdn.net/weixin_44948269/article/details/127544234