查阅 stdio.h 标准库(https://cplusplus.com/reference/cstdio/),可以发现不少输入输出函数。
这些是格式输入输出:
这些是字符(包括字符串,也即字符数组)输入输出:
这篇会介绍几个常用函数的用法,同时介绍缓冲区的概念。
文章目录
stream 的概念
stream 直译过来就是“小溪”,它形象化地描述了输入与输出的形态。由于历史原因,stream 的结构名称是FILE
而非stream
。在某些情况下文件指针(file pointer)这个术语也指stream,造成了很多误解。在stdio 中声明了FILE
类型,这个类型专门用来表示stream 对象,其中包含了所有与相关文件的联系。在实际操作中,我们只需要处理FILE指针即可。[1]
当调用程序的主函数时,它已经打开了三个预定义的stream 并可供使用。这些stream 代表已为程序建立的 "标准"输入和输出通道。包括stdin、stdout、stderr。它们的类型都是FILE *
。
输出
printf 函数
printf 函数在我的第3篇笔记里已经介绍过了。这里就附一张图回忆一下printf 的控制符:
具体的可以转到那一篇博客。
putchar 函数
函数声明如下:
int putchar ( int character );
将一个字符写入标准输出。
putchar('a');
这里注意,输出字符时要用单引号,表示这是字符。
或者输出一个char 类型的变量:
char a = 'a';
putchar(a);
这样的结果与上面的是一致的。
之前在讲到char 的时候也看到了,char 是可以用数字来赋值的,也可以用来计算。所以:
putchar(97);
这一段与上面的代码结果是一样的。
putchar 函数,包括接下来的几个函数都不是void 类型,都有返回值。putchar 函数的返回值是这样的:
成功执行之后会返回输出的字符;如果失败,将会返回EOF并设置一个错误指针(即ferror)。
EOF(End-Of-File)是一个宏,用来暗示一些错误情况。
putc fputc
在stdio库中,我们可以发现两个有意思的函数:putc 和fputc,它们的声明是这样的:
putc:
int putc ( int character, FILE * stream );
fputc
int fputc ( int character, FILE * stream );
它们的功能是:字符会被写到stream的内部位置指示器指示的位置,同时这个指示器自动前进一步。这两个函数是等同的,除了在某一些情况下putc可能会在某些库中以宏的形式去执行。putchar直接写入标准输出时也是一样的功能。
所以两个函数只有名称不同,其他方面都是相同的。只是fputc多了一个“f”,暗示它是用于文件(file)操作的。这是出于可读性的考虑。那接下来就只介绍putc好了。
在上面的解释中提到了“position indicator(位置指示器)”这个概念。它是每个stream 的内部指针,指向下一个 I/O 操作中要读取或写入的下一个字符。
现在重新回看函数声明,就会发现第二个参数是一个文件指针,指向输出的位置。一般而言这就是一个正常的文件,也就是我们在文件管理器里打开、查看、修改的文件。请看示例:
#include <stdio.h>
int main ()
{
FILE * pFile;
char c;
pFile = fopen("alphabet.txt","wt");
for (c = 'A' ; c <= 'Z' ; c++) {
putc (c , pFile);
}
fclose (pFile);
return 0;
}
在这里创建了一个名为 alphabet.txt 的文件并向里面写入了字母表。具体的方法会在文件操作里讲到。
由于stdout也是文件类型,所以调用putchar 和调用第二个参数是stdout 的putc 是等同的。
关于这两个函数的返回值:
成功执行之后会返回输出的字符;如果失败,将会返回EOF并设置一个错误指针(即ferror)。
关于返回值,两者都是成功时返回一个非负数,失败时返回一个EOF并设置一个error 指示器。
puts fputs
与前面的连个函数相比,putc 中的c 指的是“character”,而puts 中的s 指的是“string”,即字符串,所以这两个函数是用来输出字符串的。那么,类比上面的两个函数,这两个函数的差别也不大吗?
答案是:还是有一点差别的。请看声明:
puts:
int puts ( const char * str );
fputs:
int fputs ( const char * str, FILE * stream );
它们的功能都是在某个位置写入字符串,这个字符串开始于指针str 指示的位置,结束于‘\0
’(字符串休止符)。
除了与上面两个函数一样的输出位置的差别(puts 是stdout,而fputs 是自己指定的)之外,fputs不会在写入后的字符串末尾加上换行符,而puts 就会。
输入
getchar getc fgetc
这几个函数用于读取单个字符,getchar 函数用于从stdin 中读取,getc 和 fgetc 从指定的文件位置读取。
在函数定义上,由于后两种函数需要指定文件位置,而前一种函数不需要指定文件位置(已经指定了 stdin),所以会有所区别:
int getchar( void );
int getc( FILE * stream);
int fgetc( FILE * stream);
简单的运用实例是:
char c = getchar();
printf("%c", c);
可以看到只能读取一个字符。
在读取同时,内部文件指针(internal file position)会前进一个位置到下一个字符,这里的内部文件指针即函数在读取时读取字符的位置。如果了解了这个特性就可以读取所有字符了:
char c;
do
{
c = getchar();
printf("%c", c);
}while(c != '\n');
如果是从文件中读取的话就要把'\n'
改成EOF
,因为在stdin 里我们的输入是以回车结尾的,而文件里是以EOF(end of file)结尾的。
getche getch
这两个函数同样用于读取单个字符,但是并非标准库函数,而是位于Windows独有的conio.h中。这两个函数都没有使用缓冲区,也就是用户一输入,就结束函数,而不像其他函数那样需要回车。这两种函数的区别在于getche有回显,而getch没有回显。如:
char c = getche();
printf("%c\n", c);
c = getch();
printf("%c\n", c);
程序开始运行之后进入等待状态(getche在等待输入),这里的第一行的第一个1是我们自己输入的1,这个1输入完之后马上结束getche函数进入下一行;后面的一个1是第一个printf函数输出的。之后程序接着进入等待(getch在等待输入),在键盘上键入1之后没有回显,也就是这个1并不会在键盘上显示,第二行的1乃是第二个printf函数输出的。
没有回显可以干很多事,比如用在游戏制作中,读取用户的键盘数据,这种操作不需要回显;或者用在密码输入时,我们不希望输入的密码回显,显示的应该是*之类的东西。
gets fgets
gets函数的声明如下:
char * gets ( char * str );
此处的s即指字符串(string),因此这个函数就是用来输入字符串的函数。但是该函数在C11&C++14中已经不受支持。这是比较新的标准,所以在平时使用时不用太注意。至于移除的原因,则是因为gets函数在读取字符串时没有长度限制,所以会有栈溢出的隐患。在这些标准之后,gets可以说是被替换成了gets_s函数,后者读取字符串需要限制长度。[2]
gets函数读取在stdin中的字符串,在这一方面它是scanf 的平替,两者不同在于scanf 遇到空格则停止读取,而gets 函数只有在遇到换行和文件末尾时才会结束读取,简而言之就是gets 可以读取含空格的字符串。
如果读取成功,gets函数会返回str指针;如果遇到文件末尾,那么一个eof 指示器会被设置;如果文件为空,也即在文件末尾之前没有任何字符,那么返回一个空指针,str的内容也保持不变。如果读取错误,那么一个error 指示器会被设置,返回一个空指针,同时str的内容可能会改变。
在读取时,换行符不会写进str中。
使用方法很简单:
char c[30];
gets(c);
printf("%s", c);
与之前的一些函数类似,有gets函数就有fgets函数,但这两函数差距还是挺大的。以下是fgets函数的声明:
char * fgets ( char * str, int num, FILE * stream );
在参数上差距就挺大的,str是字符串写入的地址,stream则是读取的地址,而中间的num则是读取最大长度。fgets会在以下三种情况下停止读取:
- 遇到换行符
- 遇到文件末尾(end of file)
- 达到最大读取长度(num - 1)
同时fgets会将遇到的换行符认定为有效字符一并读取,因此读取后的字符串末尾是有一个换行符的。这与puts和fputs之间的区别类似。
返回值与gets相同。
可以看到fgets由于指定了读取的长度,所以免去了溢出的危害。
scanf 函数
scanf基础用法
scanf 即scan format(格式扫描),即从键盘获得输入。
它的基础用法是这样的:
char a = 'a';
fputs("Please input a character:", stdout);//主要是想复习一下前面的内容,但是puts会带上一个回车。
scanf("%c", &a);
printf("The character you have just input is %c", a);
这里用到了&
,即取地址操作符。传给scanf 函数的都得是地址,直接把变量传给它是行不通的。例如:
char a = 'a';
char *p_a = &a;
fputs("Please input a character:", stdout);
scanf("%c", p_a);
printf("The character you have just input is %c", a);
这样子写也是可以的,其实就是把取地址的操作提前了一下。
如果要输入多个变量,就有以下几种情况:
1.只用一个scanf 函数,中间以空格分隔
在这种情况下,输入时中间也必须带上空格,但是空格空多少并不一定要与scanf 函数里面一样。scanf会略过中间的空格直至下一个其他字符,在这里,空格包括空格、换行、tab[3]。例如:
int a = 0, b = 0;
scanf("%d %d", &a, &b);
printf("a = %d, b = %d\n", a, b);
scanf("%d %d", &a, &b);
printf("Now a = %d, b = %d", a, b);
2.只用一个scanf 函数,但中间用字符来间隔,如,
、、
、and
之类的。例如:
int a = 0, b = 0;
scanf("%d,%d", &a, &b);
printf("a = %d, b = %d", a, b);
分别执行以下两种操作:
如果中间不是对应字符的话就没有办法正确读取。
再比如中间是字符串的情况:
int a = 0, b = 0;
scanf("%d and %d", &a, &b);
printf("Now a = %d, b = %d", a, b);
不过需要注意的是输入时格式要相同,也就是说在scanf 中我们用的是%d and %d
,那么我们在后面输入时就要写成10 and 20
,如果在scanf 里写%dand%d
,那就得输入10and20
才行。
3. 如果中间没有空格呢?
int a = 0, b = 0;
scanf("%d%d", &a, &b);
printf("%d\n%d", a, b);
在输入时以空格为界,分隔各个变量的读取。如果没有空格,那么读取不会结束,还需要再读取一次。
示例:
scanf有没有对应的fscanf呢?也是有的!声明如下;
int fscanf ( FILE * stream, const char * format, ... );
[4]
scanf读取其他类型数据
基本上其他类型数据都与整型差不多,所以只附上控制符。除了字符串的读取需要特殊说明一下:
- scanf 是没办法读取含空格的字符串的。
- 在读取字符串的格式中字符串前可以有
&
也可以没有&
。 - 虽然字符串有两种定义方式,但是在 scanf 使用时只能用前面一种方式。
char str[] = "abc";
char *str = "abc";
下面是一个关于字符串中间没有空格的示例:
char str[30];
scanf("%s", str); //或者写作scanf("%s", &char); 但是不建议使用这种写法
printf("%s", str);
更具体的解释应该会在数组(或者字符串)部分进行解释。
scanf的一些怪异行为与缓冲区的关系
开门见山,C语言中的scanf函数使用行缓冲。
缓冲区是为了协调CPU的高效运算速度与I/O设备较慢的读取速度之间的矛盾而设计的,用以暂时保存输入输出的内容,可以提高效率。缓冲区的形式分为三种:
- 全缓冲:当达到最大存储容量时才进行真正的输入输出操作。
- 行缓冲:当遇到换行符时才进行输入输出操作。
- 无缓冲:直接进行输入输出操作,比如getche函数和getch函数。
在输出时带缓冲区的例子有:Linux下的printf函数。例如两个printf函数中间隔着一个sleep函数,两个函数最终会一起输出。与之相对地在Windows下这样做则是第一个函数先输出,等过了sleep的时间之后才会到第二个printf函数。
输入时带缓冲区的例子则更多,如果不带缓冲区用户按下一个键之后会立即执行输入操作,这样就无法输入多个字符了。
关于scanf的一些怪异行为:
首先是scanf的连续输入,这些在前面有所介绍。接下来将展示一些其他示例:
int a = 1, b = 2;
scanf("a=%d", &a);
scanf("b=%d", &b);
printf("%d\n%d", a, b);
在这里scanf中有“a=%d”
这样的字样,所以在输入时需要输入a=
。输入范例:
先输入了一个a=123之后回车,输入直接结束。
那如果我们将两个变量的输入都写进去呢?
可以看到中间有空格的不行,没有空格的可以。
如果换一下,比如把代码改成这样:
int a = 1, b = 2;
scanf("%d", &a);
scanf("%d", &b);
printf("%d\n%d", a, b);
那又回到了我们熟悉的状态。
初看这种行为可能有一点费解,但是结合了缓冲区的知识就可以理解。在以上的几种输入方式之后,缓冲区中的数据分别长这样:
a=123\n
a=123 b=456\n
a=123b=456
第一种情况下scanf读取完a之后就会接着读取b,可是开头并不是它想要的b=
,所以读取失败了,b的数据没变。第二种情况亦然。只有第三种情况是可以的。
因此我们碰到这种情况可以采用清空缓冲区的方法。以第一种情况为例,倘若有一种方法可以做到清空缓冲区,那么我们就可以在两次读取之间加上这种代码,这样\n
就会被清掉,而scanf在接下来的读取中就不会碰到它了。
清空缓冲区
输出缓冲区
有一个专门用于清空输出缓冲区的函数:
int fflush ( FILE * stream );
其形参指向一个打开的文件,比如stdout、一个打开用于写的文件或者是用于更新但是上一次操作是写操作的文件。它将会把任何没有写入的、保存在对应的缓冲区的数据写入文件中。如果指针是一个空指针,那么所有在这个程序中打开的、满足上述条件的文件都会被清空缓冲区。[5]
于是清空输出缓冲区的操作就可以是:
fflush(stdout);
输入缓冲区
没有对应的清空输入缓冲区的办法。你可以使用fflush(stdin);
这样的语句,但这并非C语言标准中的使用方法,只在Visual Studio下有定义。换言之,在其他编译器下,这个语句都是未定义的。
以下提供两种简单粗暴的方法,思路就是只要将输入缓冲区中的所有字符都读出来就可以了:
1
int c;
while((c = getchar() != '\n') && c != EOF);
2
scanf("%*[^\n]");scanf("%c");
可能这种写法会有一点难以理解,那是因为scanf的格式控制符也可以有许多高级用法。
scanf格式控制符
%{*} {width} type
这是scanf的格式控制符的完整形式:
-
type:指需要读取的数据类型;与printf的格式控制符不同的是,scanf还可以取特定字符来读取,当然这本质上是%s的拓展,例如:
1)scanf("%[abcd]", str);
表示只读取abcd四种字符,只要遇到了其他字符就会停止读取。
2)scanf("%[A-Z]", str);
表示只读取大写字母。在这里使用连字符连接两个ASCII字符,左边的字符的ASCII必须比右边小。这种写法是可以合并的,比如:scanf("A-Z0-9", &a);
取的即是大写字母与阿拉伯数字的并集。
3)scanf("%[^\n]", str);
表示不读取特定的字符,比如在这里是\n,在遇到这些字符之后就会停止读取。这一句有另外的特定的用处,在前面我们提到scanf无法读取含空格的字符串,不如gets,但这个语句是可以读取含空格的字符串的。其他的示例包括:scanf("%[^0-9]");
表示不读取数字。 -
width:表示读取最大长度。
-
*:表示丢弃所读取的字符。
那么现在来回看前面的清空缓冲区的代码,第一句就是读取除了\n之外的字符并丢弃,第二句再把\n读取并丢弃,由于输入缓冲区是行缓冲的,这样就达到了清空缓冲区的目的。