【C/C++】 stdin对字符输入的心得和经验总结(快速输入字符串)

本文探讨C++中cin与scanf的性能差异,介绍std::ios::sync_with_stdio(false)的使用以提升cin效率,详解scanf格式符%c与其他格式符的区别,以及在特定场景下的注意事项。

std::ios::sync_with_stdio(false);

用这个语句,取消cin,cout与stdio的同步,说白了就是提速,效率基本与scanf和printf一致,然后就可放心的使用cin,cout了。

在使用C/C++编写控制台应用或acm竞赛的时候,I/O方式无非是标准输入输出,特别是acm竞赛,就本人来说,由C语言入门,输入方式还只会scanf,自从学了C++,便深深地被 cin/cout输入输出流的简洁用法所吸引,相信有这种感觉的不止我一个人

  所以很长一段时间,日常的训练和各种线上比赛,再也没有使用过scanf,反手一个cin感觉很炫酷。然而好景不长,一次bestcoder的常规线上赛,前期发挥稳定,手感相当好,1001和1002快而准地ac,1003也很快来了思路(两年前的事情了,细节什么的早忘了),咔咔咔敲完代码提交ac,最后剩下充足的时间攻克1004,虽然到结束也没做出来,但是3题铁定涨分啊……

  然而终判的结果让我大吃一鲸,T!L!E!,居然超时,百思不得其解之时(其实之前知道cin效率低,但是用着太顺手了就没在意),想到了会不会是数据太多然后等着终判结束题目开放,抱着试一试的心态,把cin改成了scanf,然后……居然……秒过……

  然后网上查阅资料(只说输入,输出大同小异),cin慢的原因很多,其中很重要的一点是为了使cin与scanf可以兼容混合使用,cin在内部实现的时候会同步输入缓冲区,也就是说,输入流会时刻与输入缓冲保持同步,这是一个很耗时的操作,所以就导致了在大量输入数据的时候,cin会比scanf慢很多,可以说,这个慢,是数量级上的差异。

  如果你可以保证程序中不会出现标准输入与流输入混用的情况,可以在程序开始时使用ios::sync_with_stdio(false);关闭同步来提高速度,但是在大量数据面前输入速度仍显得乏力,相比scanf还是慢了一些(上面说的1003题我用cin关同步还是超时,只有scanf能过),个人认为原因在于对输入流对象的封装和>>这个符号的运算符重载导致执行时间变慢。

  所以从那以后,在acm生涯里再也没有使用过cin……硬生生地改回了scanf的习惯。简洁和效率总要舍弃一个,对于算法竞赛来说,效率才是关键吧……

  说了这么多cin与scanf的速度比较,接下来重点说一下scanf,用法不必多说,结合多年来竞赛经验,介绍一下格式符%c与其他格式符的区别和特定使用场景的注意事项。

  %c是一个很奇葩的设定,单独读入一个字符,包括不可见的控制字符(换行等),而其他格式化符号(如%d %lld %f %lf %s等)会在读入未完成时将换行符、空格、制表符等空白字符统统舍弃忽略,直到读到了足够的数据或遇到文件结尾才结束。我们平时控制台输入时通常按行输入,也就是输入数据后要敲击回车才能被读取,这样就导致了换行符在%c与其他格式符号并存的程序中出现各种问题,例如无法获得理想输入数据,字符串错误错误导致程序崩溃等。

  例如下面这段程序片段: 

?

1

2

3

4

5

6

<code><code><code>int a,b;

char c;

scanf("%d %d", &a, &b);

printf("a=%d b=%d\n", a, b);

scanf("%c", &c);

printf("char=%c\n", c);</code></code></code>

  理想状态下,我们输入以下数据:

  11 66a

  输出结果应该与输入一致,也就是说输入11 66后敲下回车紧接着就会出现a=11 b=66,然后再输入a敲回车会出现char=a。但事实上输入11 66敲回车得到a=11 b=66以后,并不会再等待输入字符,而是在出现char=和一个空行之后,直接结束,如下图:

  

运行结果

  为什么会出现这种情况我们先来想两个问题,为什么程序在遇到scanf等输入操作的时候,会停在那里发生阻塞为什么我们输入完成后,还要敲一下回车才能有反应

  为什么发生阻塞

  简单点说,程序在scanf处发生了I/O请求,需要数据,而scanf需要从输入缓冲区读取数据,程序刚运行的时候,这个缓冲区是空的,所以scanf得不到数据,就会阻塞程序,一直等待缓冲区内出现数据,此时我们从控制台输入内容,敲下回车,输入的内容便会传送给程序输入缓冲区,被scanf阻塞的程序发现缓冲区里有内容了,就会让scanf继续执行,读入数据。

  为什么要敲回车

  默认情况下,我们在控制台的输入内容是不会立刻同步到缓冲区的,也许是为了防止误输入或效率问题,只有敲下回车的时候,输入内容连同换行符才会被一起传送至缓冲区,但实际上,被传送至缓冲区的换行符通常是我们所不需要的,它只是我们从控制台输入内容时所要按下的一个键而已,并不是我们需要的数据。

  当缓冲区内有了数据,scanf便开始按照设定的格式进行读取,除%c格式符以外,scanf会按照格式里的内容从左至右读取指定格式的数据。

  我们回看之前的例子,单步解读程序,格式内容"%d %d",我们输入11 66敲回车后,输入缓冲区内容变为11 66\n(用\n代表换行符),scanf先尝试读取格式内容里的第一个%d,也就是读整数,从缓冲区里成功读到了11,此时缓冲区剩下66\n(注意开头有个空格),然后尝试读取第二个%d,由于此时缓冲区开头的内容是空格,%d不理睬 ,忽略开头若干空白字符,然后遇到66,成功读入,此时scanf没有别的要读的了,结束,函数返回2(读入的数量),此时缓冲区剩余\n,然后程序执行printf,再执行scanf("%c", &c);,由于此时缓冲区有内容\n,所以不阻塞,直接读取,开头说到%c会读取一切字符,所以换行符自然而然被读到了 ,所以变量c的内容是'\n',带入printf("char=%c\n", c);,就可以明白为什么会出现图示的现象了。

  如何解决方案就是在每次使用scanf之后,调用fflush(stdin);来清空输入缓冲区(也就是清掉那个恼人的换行符),然而这样做很麻烦并且几乎所有oj都拒绝这种危险的操作,所以一般使用getchar();来消除换行符(实际就是读入但不赋值给变量)。

  getchar()与scanf("%c")一样,可以读入任意字符,所以我们每次要使用scanf("%c")时,不妨先检查在此之前是否有其他的输入行,如果有的话,记得在这两次输入之间加上getchar()来抵消敲击回车所产生的换行符。上例,修改后如下: 

?

1

2

3

4

5

6

7

<code><code><code>int a,b;

char c;

scanf("%d %d", &a, &b);

printf("a=%d b=%d\n", a, b);

getchar();

scanf("%c", &c);

printf("char=%c\n", c);</code></code></code>

  运行,我们依次输入11 66,敲回车,输入a,敲回车,结果如下:

  

运行结果2

  这下就正常了。

  下面是一个典型的输入场景:

  矩阵类字符输入

  描述: 第一行输入两个整数n,m,接着n行,每行m个字符

  样例: 

?

1

2

3

<code><code>2 3

abc

def</code></code>

  分析:

  我们先不考虑怎么一步步输的,假设把整个输入内容送到缓冲区,然后我们缓冲区内的字符序列是这样的:2 3\nabc\ndef\n,观察这个序列,a,d前面都有换行,所以读取这两个字符时,换行会产生干扰,当然最后一个换行程序都快结束了留着也没什么卵用,所以我们需要抵消三个换行,分别在输完2 3以后和每行的字符输完以后。

  实现1:  

?

1

2

3

4

5

6

7

8

9

10

11

12

<code><code>char a[10][10];

int n,m;

scanf("%d %d", &n, &m);

getchar(); // 接下来要读字符,而这里产生了额外的换行,吃掉

for(int i = 0 ; i < n ; i++)

{

    for(int j = 0 ; j < m ; j++)

    {

        scanf("%c", &a[i][j]);

    }

    getchar(); // 读完了m个字符,也就是一行,产生了一个换行符,而接下来的外层循环要读下一行的字符,所以要吃掉它

}</code></code>

  实现2(输入不包含空格):

  如果题目明确表示或暗示输入字符不包含空格,可以将每行字符作为一个不含空格的字符串输入,也就是使用scanf(“%s”),这样我们就不用考虑换行符的抵消问题了,因为前面说过,除了%c,其他格式符号都不会care换行符。

  下面实现的前提是字符中不包含空格  

?

1

2

3

4

5

6

7

char a[10][10];

int n,m;

scanf("%d %d", &n, &m);

for(int i = 0 ; i < n ; i++)

{

    scanf("%s", a[i]);

    //  这样相当于直接将abc按照顺序依次存入a[0][0],[0][1],[0][2],并自动在a[0][3]加入字符串结束标志'\0',方便调试。

  实现3(包含空格但不想用scanf):

  如果觉得scanf太麻烦,而且输入的字符的确包含空格,那么可以用gets()函数,不过这个函数由于在设计时存在缓冲区溢出漏洞,C++标准里并不推荐使用,但是日常训练和竞赛只要稍加注意并不会出现溢出问题(只要字符数组够大就没事),所以这个函数也是很好用的。

  gets()函数会读入一行字符,它会一直读输入缓冲区内的字符序列直到遇到了'\n'才会停止,值得注意的是,gets()以换行符为界,并不会把换行符作为输入的一部分而读进字符串,但会消耗掉换行符,这点与scanf有所不同。

  例如输入缓冲区内容为abc def gh 666\n233,gets函数从该缓冲区读取到的内容为abc def gh 666,而缓冲区剩余233,'\n'为gets函数做“路标”但惨遭gets函数“抛弃”。当然,如果缓冲区第一个字符就是换行符,则gets会读入一个空字符串并消耗掉缓冲区的这个换行符。上例代码: 

?

1

2

<code><code><code>char a[10][10];

int n,m;</code></code></code>

  scanf("%d %d", &n, &m);getchar(); // 此时缓冲区第一个字符是换行符,不消除的话下一个gets会读到空字符串for(int i = 0 ; i < n ; i++){ gets(a[i]); // 这里不需要为下一次循环的gets做换行抵消,因为gets本身就会在本行输入结束时抵消换行符}

  总结来说,了解了缓冲区的作用和各种输入对于控制符的处理,再处理起字符类输入问题就得心应手了,acm生涯里不乏遇到各种姿势的变态输入格式,有的时候数据输入完了再输出来就变得不一样了,还有的时候输入数据复杂到整个题目的时间都用在了研究输入上,所以掌握好基本的数据输入才是ac的第一步。以上内容为转载内容,地址:https://blog.youkuaiyun.com/ShannonNansen/article/details/76090172个人补充:关于缓存区问题还需要注意是否关闭与它的影响,比较熟悉的便是cin缓冲区所造成的超时,但其实还会在与printf混用时(基础不好,个人喜欢混用处理)乱序输出;#include

  using namespace std;

  int main()

  {

  ios::sync_with_stdio(false);

  cout<<"aaa";printf("bbb");

  return 0;

  }输出结果:bbbaaa

  造成这种现象的原因便是因为缓冲区中存在bbb。可以在cout后加flush清除缓冲区或在头文件加入stdio.h均可

在 C/C++ 中统计字符串中每个字符的出现次数,可以通过使用标准库中的 `map` 或 `unordered_map` 来实现。以下是一个基于 C++ 的实现示例,使用 `map` 来统计每个字符的出现频率: ```cpp #include <iostream> #include <string> #include <map> using namespace std; int main() { string s; cin >> s; map<char, int> m; for (auto &i : s) { m[i]++; } for (auto &i : m) { cout << i.first << " 出现了 " << i.second << " 次" << endl; } return 0; } ``` 该程序通过遍历字符串中的每个字符,并使用 `map` 记录每个字符的出现次数。`map` 是一个有序的关联容器,其键值对按照字符的 ASCII 值排序。如果希望使用无序的映射以提高性能,可以改用 `unordered_map` [^1]。 如果使用 C 语言,可以使用数组来统计每个字符的出现次数。例如,对于 ASCII 字符集,可以使用大小为 128 的数组来记录每个字符的计数: ```c #include <stdio.h> #include <string.h> int main() { char str[100]; int count[128] = {0}; // 支持所有 ASCII 字符 printf("请输入字符串:"); fgets(str, sizeof(str), stdin); for (int i = 0; i < strlen(str); i++) { count[(int)str[i]]++; } for (int i = 0; i < 128; i++) { if (count[i] > 0) { printf("字符 '%c' 出现了 %d 次\n", i, count[i]); } } return 0; } ``` 该方法利用字符的 ASCII 值作为数组索引,直接访问并更新对应的计数器。这种方式在字符集有限的情况下非常高效 [^4]。 ### 相关问题 1. 如何在 C++ 中使用 `unordered_map` 优化字符计数的性能? 2. 如何统计字符串中仅字母字符的出现次数? 3. 如何在 C/C++ 中处理 Unicode 字符的计数? 4. 如何对字符计数的结果进行降序排序? 5. 如何编写一个函数返回指定字符字符串中的出现次数?
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值