【记录】C/C++-关于I/O的坑与教训
吐槽
每每读取字符串时,倘若稍有灵活的操作,总会遇上诡异奇怪的事情。究其原因,就是没完全理解一些基本读写函数的机制。这次做Uva227就把I/O上的问题全暴露出来了。想来还是应该记录一些经验教训。
记录
操作系统的缓冲区管理机制,是一切奇怪事件的罪魁祸首!用户输入的数据将先进入缓冲区,待键入'\n'后,系统才会一次性将缓冲区的内容推给函相关数处理。如果不能准确把握缓冲区内容与变动,便无法精准高效的完成读取和存储。而缓冲区内容的变动,和每一个函数的机制有关。
1. scanf中的格式化字符串
scanf(<格式化字符串>[,地址表]);
格式化字符串中包含三类字符:格式说明符、空白字符(空格、\n、\t等)、非空白字符。问题往往产生于格式说明符和空白字符之间。
"%s": 读取缓冲区中从当前位置开始第一个非空白字符到下一个空白字符之间的内容,存入相应变量(会补上'\0',所以不用担心该字符串的初始化)。之前的空白字符将被读取并忽略,但之后的空白符不会被读取清理,仍保留在缓冲区中(这也表明空白字符不会存入字符串中,让我们省心不少)。
"%[]": 应用扫描字符集之后要注意,不符合要求的字符将仍会留在缓冲区中。举例而论,使用scanf("%[^\n]")
读取之后,缓冲区中至少会剩下'\n',如果不注意的话,由于空白字符的诡异性会给之后的I/O造成障碍。因而最好的办法是之后紧跟一个%*c
丢弃其后一个字符。scanf("%[^\n]%*c")
而不使用scanf("%[^\n]\n")
,是因为忌惮空白字符的怪异读取性质。
"\n"等空白字符: 除去%c
,scanf中对缓冲区中空白字符都采取“一视同仁,消极忽略”的态度,任何一个空白字符都能代表其他所有的空白字符。scanf("\n")
、scanf(" ")
等都是一个含义:读取并忽略此后的空白字符(串)。
同时,对空白字符的读取操作未必是即时的:当缓冲区中只剩下类似'\n'的空白字符时,scanf并不会进行操作;等到下一个非空白字符出现,才会开始处理。例如:
char s[10];
scanf("%s\n",s);
printf("%s");
操作前缓冲区 | 操作后缓冲区剩余 | 完成操作解释 |
---|---|---|
"asd\n" | "\n" | %s将"asd"读取并存入s中,因为只有空白字符"\n"并未被匹配读取 |
"\nd\n" | "d\n" | 由于缓冲区键入了"d",scanf开始运作,前一个"\n"被匹配读取。最后缓冲区残余"d\n" |
"%c": 最单纯的一种,把一切当作平等的字符,既不多读也不少读——因此常用来清理缓冲区丢弃残余字符。%*c
代替\n
清理空白字符,因为后续若直接键入空格,使用\n
可能会被当作空白字符直接丢弃,导致事故发生。
2. fgets中读取size
char str[SIZE];
fgets(str,SIZE,stdin);
当我们规定fgets只能读取SIZE个字符时,它会贴心的为'\0'预留出最后一个位置。因此,字符串str最多只能读取存入SIZE-1个实体字符。
同时,fgets并不会歧视所读取到的"\n",会将其存入目标字符串str中。简而言之,fgets后str中末尾的"\n"(小心Linux下读取Windows文件时的"\r\n",似乎和语言特性有关,先不管)来自于缓冲区而非函数自己添加。所以当缓冲区内容超过SIZE-1个时,其后的内容会残留,也包括最后的"\n"。
3.string类型行读取
istream& getline(istream& is,string& str[,char delim]);//delim默认为'\n'
会丢弃末尾的'\n'。按此机制,如遇空行,则丢弃换行符,str中内容为空。返回值:只有当读取失败(如EOF)才会为假,其余情况都会为真,while中都会继续循环
则忽视空行的写法
while( getline(cin,str),str.size()==0 );
4.cin,cin.getline()与cin.get()
1.两个函数从缓冲区中读取小于等于ASIZE个字符,遇到'\n'时终止。
cin.getline(arr,ASIZE)
:读取'\n',并将其替换为'\0';读取ASIZE个字符后强制结束,并将末位替换位'\0',同时设置failbit,阻断cin之后读取,可以通过cin.clear()
清除;遇空行,不会设置failbit。
cin.get(arr,ASIZE)
:不读取'\n',会将其留在缓冲区,读入段后添加'\0';读取ASIZE个字符后强制结束,并将末尾替换为'\0',但不会设置failbit;遇空行,却会设置failbit。
cin.get()
:重载了函数,读取下一个字符,包括换行符,可以借此消除cin.get遗留的换行符
/*利用函数重载和类的特性*/
cin.get(arr,ASIZE).get();
cin.getline(arr1,S1).getline(arr2,S2);
2.cin >>
类似scanf(%s)
,读取从当前位置开始第一个非空白字符到下一个空白字符之间的内容。
所以它会将换行符留在缓冲区,因而混合数据类型读取时需要注意,可以用前面的方法解决
(cin >> year).get();
令人火大的实例
调试了几乎一天的题目:UVa227 Puzzle。考的就是输入输出
改的千疮百孔,好些地方可以优化。
#include<stdio.h>
#include<string.h>
char map[5][6];
int x,y;
void findSpace();
int move(int i,int j);
int main(){
int cnt=0;
while(fgets(map[0],6,stdin)!=NULL && map[0][1] && map[0][1]!='\n')/* 注意有空格 */ /* 文件Z后是否有\n,也要考虑 */
{
if(map[0][4]!='\n') scanf("%*c"); /* 考虑到倘若第一行没空格读满,则"\n"会留在缓冲区,若不清理,则其后的扫描字符无法读取 */
scanf("%[^\n]%*c%[^\n]%*c%[^\n]%*c%[^\n]",map[1],map[2],map[3],map[4]);
/* 注意scanf("\n");的运作形式 */
for(int i=0;i<5;++i)
if(map[i][4]=='\n') map[i][4]=' ';
/* 处理空格在行末的情况,UVa样例输入里没有给出空格直接换行了;还打多了一个等号 */
if(cnt) puts(""); /* 非统一空行 */
printf("Puzzle #%d:\n",++cnt);
findSpace();
char cmd=0;
int isok=1; printMap();
while((cmd=getchar())!=EOF && cmd!='0')
{
if(isok==0 || cmd=='\n') continue;
switch(cmd){
case 'A':isok=move(-1,0);break;
case 'B':isok=move(1,0);break;
case 'L':isok=move(0,-1);break;
case 'R':isok=move(0,1);break;
default :isok=0; /* 根据Uva英文输入的描述,非法操作包括输入其他字符 */
}
// if(!isok){
// printf("This puzzle has no final configuration.\n");
// break; /* 如果这里就break的话,后续还有待输入的序列残留缓冲区,会影响后续IO */
// }
}
if(isok)
{
for(int i=0;i<5;++i){
for(int j=0;j<5;++j){
putchar(map[i][j]);
if(j!=4) putchar(' ');
}
puts("");
}
}
else printf("This puzzle has no final configuration.\n");
scanf("%*c");
}
return 0;
}
void findSpace(){
for(x=0;x<5;++x) for(y=0;y<5;++y)
if(map[x][y]==' ') return;
}
int move(int i,int j){
x+=i,y+=j;
if(0<=x && x<5 && 0<=y && y<5){
map[x-i][y-j]=map[x][y];
map[x][y]=' ';
return 1;
}
else return 0;
}