NOIP学习小站
西安交通大学附属中学航天学校

字符串的输入输出

通过前面的学习可知,可以使用字符数组来存储字符串,也可以直接使用C++的string类型来存储字符串。但是如果要按行输入带有空格的字符串,只是简单地使用cin和scanf是不能实现的。

本小节重点研究字符串的输入,重点处理按行输入有空格的字符串的情况。

现实生活中,编程解决实际问题时,要处理的字符和字符串数据其实远多于数字类型;在信息学竞赛中,字符串的处理也是一个重点。

一、字符串的输入输出

不管是用字符数组存储字符串还是直接使用C++的string类型存储字符串,都可以使用cin输入字符串、cout输出字符串:

#include<iostream> 
using namespace std;
int main()
{
    char str[1010];
    cin>>str;
    cout<<str<<endl;
    return 0;
}
#include<iostream>
#include<string> 
using namespace std;
int main()
{
    string str;
    cin>>str;
    cout<<str<<endl;
    return 0;
}

不能使用scanf和printf输入输出string类型字符串,但是可以使用scanf和printf输入输出存储在字符数组中的字符串,还可以使用puts函数输出存储在字符数组中的字符串:

#include<cstdio>
int main()
{
    char str[1010];
    scanf("%s",str);  //不能用&str 
    printf("%s\n",str);
    puts(str);	    //puts函数输出字符串,输出字符串后会自动换行 
    return 0;
}

二、按行输入有空格的字符串

不管是使用scanf还是cin输入字符串,空格依然是多个数据间的分隔符,如果想输入一行包括空格的字符串,直接简单地使用scanf和cin是不能实现的,还是上面的代码:

#include<iostream> 
using namespace std;
int main()
{
    char str[1010];
    cin>>str;
    cout<<str<<endl;
    return 0;
}
#include<iostream>
#include<string> 
using namespace std;
int main()
{
    string str;
    cin>>str;
    cout<<str<<endl;
    return 0;
}

想输入一行包括空格的文本,例如Hello World,运行程序,输入Hello World,输出结果都是Hello,可知str只接收到了Hello World空格前的Hello。那么如何按行输入有空格的字符串呢?

1.使用字符数组存储字符串的情况

与puts函数输出用字符数组存储的字符串相对应的是,使用gets函数将字符串输入到字符数组中保存,gets函数能够按行输入,即使行内有空格也不受影响:

#include<cstdio> 
int main()
{
    char str[1010];
    gets(str);
    puts(str);
    return 0;
}

输入Nice to meet you!,输出结果如下:

Nice to meet you!

可知gets函数能够很好地实现按行输入,即使行内存在空格。

注意:gets函数有内存泄漏的风险,并且竞赛使用的C++版本已经移除了gets函数,所以不推荐使用!

对于C,推荐使用fgets函数读入一行,不过要注意fgets函数往往会导致接收到的字符串末尾会有多余的换行符'\n',需要特殊处理:

#include<iostream>
#include<cstring>
using namespace std;
int main()
{
    char str[1010];    //最好多开几个
	fgets(str,sizeof(str),stdin);    //注意fgets的使用方法
	
	int n = strlen(str);
	//处理末尾可能额外附带的'\n'
	if(n && str[n-1]=='\n') str[--n] = '\0';
	
	cout<<str<<" "<<strlen(str)<<endl;
    return 0;
}

还可以使用C++中的cin.getline读入一行:

#include<iostream>
#include<cstring>
using namespace std;
int main()
{
    char str[1010];
	cin.getline(str,sizeof(str));    //注意cin.getline的使用方法	
	cout<<str<<" "<<strlen(str)<<endl;
    return 0;
}

2.使用string类型的情况

使用string类型存储字符串,可以使用getline函数读入一行内容,即使行内有空格也不受影响:

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string str;
	getline(cin,str);
	cout<<str<<" "<<str.length()<<endl;
    return 0;
}

3.使用getchar重复接收输入直到遇到换行

可以使用getchar函数每次读取一个字符存入到字符数组中,直到读到换行符'\n'为止:

#include<iostream>
#include<cstring>
using namespace std;
int main()
{
    char str[1010],ch;
	int tot = 0;
	while(true){
	    ch = getchar();
	    if(ch=='\n') break;
	    str[tot++] = ch;
	}
	str[tot] = '\0';
	cout<<str<<" "<<strlen(str)<<endl;
    return 0;
}

上面的代码可以进一步精简:

#include<iostream>
#include<cstring>
using namespace std;
int main()
{
    char str[1010],ch;
	int tot = 0;
	while((ch = getchar()) != '\n') str[tot++] = ch;
	str[tot] = '\0';
	cout<<str<<" "<<strlen(str)<<endl;
    return 0;
}

其实当输入的数据量很大的时候,使用getchar的效率反而是更高的!与输入字符getchar()函数对应的是putchar()函数,用于输出字符,例如putchar(ch);,将以字符的形式输出变量ch。

三、字符串输入出现异常的处理

来看一个简单的问题,输入正整数\(n\),然后输入\(n\)个包含空格的字符串,输出每个字符串的长度。很自然写出下面的程序(分别是string和char数组存储字符串):

#include<iostream>
#include<string>
using namespace std;
int main()
{
    int n;
    string str;
    cin>>n;
    for(int i=1;i<=n;i++){
    	getline(cin,str);
    	cout<<str.length()<<endl;
	}
    return 0; 
}
#include<iostream>
#include<cstring>
using namespace std;
char str[1010];
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
    	fgets(str,sizeof(str),stdin);
    	int len = strlen(str);
    	//处理末尾可能额外附带的'\n'
    	if(len && str[len-1]=='\n') str[--len] = '\0';
    	cout<<strlen(str)<<endl;
	}
    return 0; 
}

运行上面的程序,输入测试数据与结果如下:

尝试输入数据:

2
Jim Green
Han Meimei

执行结果:

运行时会发现,输入了第一个字符串回车,还没有输入第二个字符串,程序就结束运行了。是什么原因导致出现这样意外的运行结果呢?

仔细分析运行结果可知,循环体内第一次getline(或fgets)输入的字符串竟然是一个空字符串""(所以输出长度为0),应该是前面输入整数并回车导致循环体内第一次不能正常输入字符串,getline(或fgets)会将整数后回车前的内容(其实就是一个空字符串)输入到字符串中,这样奇葩的处理方式真的是防不胜防呀!

好在我们在运行测试的过程中就能发现这个问题并可以想办法解决。这里也给我们一个提醒:字符和字符串的输入一定要小心,往往会出现防不胜防的异常输入情况,特别是在字符字符串与其他类型数据混合输入的时候。

回到上面的问题,其实我们可以在cin>>n;语句后添加一句getchar();来接收一个字符,就能很好地避免上面出现字符串输入异常的情况。

*四、scanf函数的神奇用法:扫描字符集合

其实scanf函数还有很多神奇的用法,例如使用scanf("%[^\n]",str);也能输入有空格的一行字符串,格式符中^表示非,^\n表示不是回车一直读,遇到回车结束。

char str[1010];
scanf("%[^\n]",str);
printf("%s %d",str,strlen(str));

但是用"%[^\n]"作为输入格式符,要多次输入字符串,例如输入两个字符串,要注意接收回车,看下面的程序:

char c[1001],s[1001];
scanf("%[^\n]",c);
scanf("%[^\n]",s);

上面的程序,不接收回车,那么数组s就输不进去,想输进去就要在第1个输入语句scanf("%[^\n]",c);之后加个getchar();接收回车; getchar();也可以换成scanf("%*c");scanf("%*c");的作用是读入一个字符但不保存到变量。

所以多组输入带空格的字符串存储到字符数组可以这样写:

char str[1010];
while(scanf("%[^\n]%*c",str)!=EOF){
    printf("%s %d\n",str,strlen(str));
}

*五、扩展:扫描字符集合

扫描字符集合是scanf函数用于字符串读取的一个工具,它可以比%s更灵活地控制读取过程,具体如下:

1.%[]的中括号中需要填写一个正则表达式,用于指明只读取哪些字符或者不读取哪些字符

2.当中括号内的内容不是以^开头的时候,表示只读取在中括号中出现的内容,当遇到第一个没有出现的字符时,就停止读取,并把目前已经读取的内容保存到对应的字符数组中,例如:

char s[110];
scanf("%[0-9]",s); //只读取数字

假设输入为:123a456,那么上面的scanf将把123读取并保存到s数组中,其余的a456将遗留在缓冲区中。

如果把上面的scanf()调用改为如下形式:scanf("%[13579]",s); 并且输入如下:123,那么将只读取1,并把它做为字符串保存到s 中,其余字符将遗留在缓冲区中,因为第二个字符'2'没有出现在扫描集中,所以不再继续读取。

3.如果扫描集的第一个字符是^,那么读取规则就变成了只读取没有出现在扫描集中的字符,遇到第一个出现在扫描集中的字符时,读取即告停止,例如: scanf("%[^0-9]",s); 这个调用将只读取非数字字符,遇到数字字符时读取停止,如果输入的是:abc2020noip,那么将读取abc到s数组,其余的字符将遗留在缓冲区中。