【算法竞赛】C/C++ 的输入输出你真的玩会了吗?

【算法竞赛】C/C++ 的输入输出你真的玩会了吗?
在这里插入图片描述
🔭 个人主页:散峰而望

《C语言:从基础到进阶》《编程工具的下载和使用》《C语言刷题》《算法竞赛从入门到获奖》《人工智能AI学习》《AI Agent》

愿为出海月,不做归山云


🎬博主简介

在这里插入图片描述
请添加图片描述

文章目录


前言

在编程的世界中,输入输出(I/O)是与用户或外部系统交互的基础。C 和 C++ 提供了强大的 I/O 机制,从简单的 printf 和 scanf 到 C++ 的流式操作 cin 和 cout,看似简单,实则暗藏玄机。许多开发者在使用过程中可能会遇到缓冲区问题、格式控制陷阱、性能瓶颈,甚至安全性隐患。
本文将对 C/C++ 一些关于在输入输出的问题进行一一打击。

1. OJ(online judge)题目输入情况汇总

在竞赛的 OJ 题目中,一般关于输入场景总结为下面四类:

在这里插入图片描述

由于单独讲解可能体验不是那么深,所以接下来,我们就结合题目,给大家分别介绍。

1.1 单组测试用例

  1. 计算 (a+b)/c 的值
在这里插入图片描述

参考代码:

#include<iostream>usingnamespace std;intmain(){int a, b, c; cin >> a >> b >> c; cout <<(a + b)/ c << endl;return0;}
  1. 与 7 无关的数
在这里插入图片描述
思路
先输入 n,之后循环产生 1~n 的数字,接着找出与 7 无关的正整数,最后求平方和。
那么如何产生与 7 无关的正整数呢?
我们不如先找与 7 相关的正整数,然后取反就能得到与 7 无关的正整数。
又由题目知道 n < 100,所以只需在 100 内的数字进行判断。
即与 7 相关的条件有:被 7 整除,%10 == 7(个位),/10 == 7(十位)

参考代码:

#include<iostream>usingnamespace std;intmain(){int n; cin >> n;int i =1;int sum =0;while(i <= n){if(i %7!=0&& i %10!=7&& i /10!=7){ sum +=(i * i);} i++;} cout << sum << endl;return0;}

1.2 多组测试用例

1.2.1 测试数据组数已知

  1. 多组输入a+b II
在这里插入图片描述

参考代码:

#include<iostream>usingnamespace std;intmain(){int n;//表示数据组数 cin >> n;int a, b;//每行输入的a,b值 while(n--){ cin >> a >> b; cout << a + b << endl;}return0;}
  1. 斐波那契数列
斐波那契数列:1 1 2 3 5 8 13 21 34 55 …
特点:前两个数的和是第三个数
在这里插入图片描述
在这里插入图片描述

斐波那契有多种做题办法

参考代码:

  • 正常的循环嵌套:
#include<iostream>usingnamespace std;intmain(){int n, a; cin >> n;while(n--){ cin >> a;int x =1;int y =1;int z =1;while(a >2){ z = x + y; x = y; y = z; a--;} cout << z << endl;}}
  • 由题目可知,题目把所求范围限制在 1~30 之间,所以提前算出前 30 个斐波那契数,存储下来输入 Q 后,直接拿取第 a 个斐波那契数:
#include<iostream>usingnamespace std;intmain(){int n, a, i;int ret[35]={0,1,1};for(i =3; i <30; i++){ ret[i]= ret[i -1]+ ret[i -2];} cin >> n;while(n--){ cin >> a; cout << ret[a]<< endl;}return0;}
  • 还有一种方法是递归,后面会进行讲解
  1. 制糊串
在这里插入图片描述
在这里插入图片描述
思路:输入 s,t,qq 次询问
while(q–)
{ }
处理一组数据
输入 l1,r1,l2,r2取出子串 s1,t1比较子串的大小,并输出
字符串的比较 - 支持关系运算的比较

参考代码:

#include<iostream>#include<string>usingnamespace std;intmain(){ string s, t; cin >> s >> t;int q; cin >> q;int l1, r1, l2, r2;while(q--)// 这种写法是常?的处理 q 次询问的方式 { cin >> l1 >> r1 >> l2 >> r2; string s1 = s.substr(l1 -1, r1 - l1 +1);// 注意这道题的字符串是从 1 开始计数的  string t1 = t.substr(l2 -1, r2 - l2 +1);if(s1 < t1){ cout <<"yifusuyi"<< endl;}elseif(s1 > t1){ cout <<"erfusuer"<< endl;}else{ cout <<"ovo"<< endl;}}return0;}
编程技巧:
题目中说"有 q 次询问",意思是程序要处理q组测试数据,(也就是对应 q 次循环),我们要针对每次询问,给出一个结果。
其实就是之前的单组测试变成了 q 组测试,在之前的代码上套一层 while 循环即可。当有 q 次询问的时候,while(q–) 是非常方便的方式。然后就按照单组输入的方式处理每组输入的数据就好了。

1.2.2 测试数据组未知

  1. 多组输入a+b
在这里插入图片描述

参考代码:

#include<iostream>usingnamespace std;intmain(){int a, b;while(cin >> a >> b){ cout << a + b << endl;}return0;}
📌cin >> a; 会返回一个流对象的引用,即 cin 本身。在 C++ 中,流对象 cin 可以被用作布尔值来检查流的状态。如果流的状态良好(即没有发生错误),流对象的布尔值为 true。如果发生错误(如遇到输入结束符或类型不匹配),布尔值为 false。在 while (cin >> a >> b) 语句中,循环的条件部分检查 cin 流的状态。如果流成功读取到 2 个值,cin >> a >> b 返回的流对象 cin 将被转换为 true,循环将继续。如果读取失败(例如遇到输入结束符或无法读取到2个值),cin >> a >> b 返回的流对象 cin 将被转换为 false,循环将停止。
  1. 数字三角形

参考代码:

#include<iostream>usingnamespace std;intmain(){int n;while(cin >> n){for(int i =1; i <= n; i++){for(int j =1; j <= i; j++){ cout << j <<" ";} cout << endl;}}}
  1. 定位查找
在这里插入图片描述
思路:先输入 n,指明要存储多少个数
2.再输入 m,并和输入的数组中的数据进行比较查找
3.只要查找的一出现就输出并跳出循环,如果没有就输出 No

参考代码:

#include<iostream>usingnamespace std;constint N =25;int arr[N];intmain(){int n, m;while(cin >> n){for(int i =0; i < n; i++){ cin >> arr[i];} cin >> m;int i =0;for(i =0; i < n; i++){if(m == arr[i]){ cout << i << endl;break;}}if(i == n){ cout <<"No"<< endl;}}return0;}

1.2.3 特殊值结束测试数据

  1. 字符统计
在这里插入图片描述

参考代码:

//代码1#include<iostream>usingnamespace std;intmain(){int ch =0;int Letters =0;int Digits =0;int Others =0;while((ch =getchar())!='?'){if((ch >='a'&& ch <='z')||(ch >='A'&& ch <='Z')){ Letters++;}elseif(ch >='0'&& ch <='9'){ Digits++;}else{ Others++;}} cout <<"Letters="<< Letters << endl; cout <<"Digits="<< Digits << endl; cout <<"Others="<< Others << endl;return0;}
//代码2#include<iostream>usingnamespace std;intmain(){ string s;int Letters =0;int Digits =0;int Others =0;getline(cin, s); s.pop_back();//去掉? for(auto ch : s){if((ch >='a'&& ch <='z')||(ch >='A'&& ch <='Z')) Letters++;elseif(ch >='0'&& ch <='9') Digits++;else Others++;} cout <<"Letters="<< Letters << endl; cout <<"Digits="<< Digits << endl; cout <<"Others="<< Others << endl;return0;}

这里判断大小写字母数字可以用一种简单的方法来判断,运用函数:

if(islower(ch)||isupper(ch)) Letters++;elseif(isdigit(ch)) Digits++;else Others++;

当然判断字母还能再简化一下

if(isalpha(ch)) Letters++;elseif(ch >='0'&& ch <='9') Digits++;else Others++;

可以拓展了解一下 :isalphaisdigit

  1. 多组数据a+b III
在这里插入图片描述

参考代码:

#include<iostream>usingnamespace std;intmain(){int a =0, b =0;while(cin >> a >> b, a && b){ cout << a + b << endl;}return0;}
这里的逗号表达式简单介绍一下:从左往右依次计算整个表达式的结果是最后一个表达式的结果

OK 了,有关OJ(online judge)题目输入情况的所有情况都讲完了,希望各位友友们在做题中能够有着清晰的判断。

2. 输入时特殊技巧

2.1 含空格字符串的特殊处理方式

根据我们现在掌握的知识,含空格的字符串,如要读取有 fgets、scanf、getchar、getline 四种方式解决,但是有时候,根据题目的情况,不一定非要完整的读取这个带空格的字符串,而是将字符串中空格隔开的每一个字符串,当做一个单词处理更方便,也避免了读取带空格字符串的各种问题。

练习:统计数字字符个数

在这里插入图片描述

这里我们有两种做法:

整个字符串一次性读取,然后处理字符串,使用 getline使用逐个单词的方式处理 cin
  1. 法一:读取整个带空格的字符串分析
#include<iostream>#include<string>usingnamespace std;intmain(){ string s;getline(cin, s);int ret =0;for(auto ch : s){if(ch >='0'&& ch <='9'){ ret++;}} cout << ret << endl;return0;}
  1. 法二:按照多个单词分析
#include<iostream>#include<string>usingnamespace std;intmain(){ string s;int cnt =0;while(cin >> s){for(auto c : s){if(c >='0'&& c <='9') cnt++;}} cout << cnt << endl;return0;}

同时由上面的部分介绍,我们可以把

if(ch >='0'&& ch <='9')

修改为

if(isdigit(ch))

2.2 数字的特殊处理方式

当我们程序运行的时候,在控制台输入123 的时候,这时的 123 是三个字符,123 是一个字符序列,程序会根据代码中的数据类型,可能将123解析成整型,也可能将 123 解析成字符串。

比如:

int num =0; cin >> num;//输入123, 就被解析成整数  string s; cin >> s;//输入123, 就被解析成字符串

这里的解析的方式,主要是依赖编译器对变量类型的识别,根据类型再将读取字符串数据转化成对应类型的数据。

我们在写代码的时候,应该根据实际的情况,来决定如何处理输入的内容。

练习:小乐乐改数字

在这里插入图片描述
在这里插入图片描述
  1. 法一:当做整数读取
输入一个整数 n获取 n 的每一位
% 10,判断是奇数(1)还是偶数(2)
/ 10,求后面的数字
3.输出转换后的整数
#include<iostream>#include<cmath>usingnamespace std;intmain(){int n;int i =0, ret =0; cin >> n;while(n){if(n %10%2==1){ ret +=pow(10, i);} n /=10; i++;} cout << ret << endl;return0;}
  1. 法二:当做字符串处理
由于读取的是字符,所以 1,2,3 不能当作字符来判断,而是用 ASCII 判断
又 ‘0’ = 48,‘1’ = 49,‘2’ = 50,‘3’ = 51 … 发现 ASCII 为偶数时对应的正好为偶数,奇数时正好为奇数,故可以 %2 判断。
#include<iostream>#include<string>usingnamespace std;intmain(){ string s; cin >> s;for(int i =0; i < s.size(); i++)// 数字字符与对应的数的奇偶一致 {if(s[i]%2){ s[i]='1';}else{ s[i]='0';}} cout <<stoi(s)<< endl;// 转换成数字输出 return0;}
这里说一下代码的一些函数pow(a, b) 求 a 的 b 次方stoi 是 C++11 标准引入的字符串转换函数,用于将字符串转换为整数

3. scanf/printf 和 cin/cout的对比

scanf 和 printf 是 C 语言中的标准输入输出函数,而 cin 和 cout 是 C++ 语言中的标准输入输出流对象。它们各自有优缺点,整体上来说 cin 和 cout 会更加方便,但有时候我们也不得不使用 scanf 和 printf。

3.1 格式控制差异

  • scanf 和 printf 不能自动识别输入数据的类型,需要手动指定格式字符串,容易出现格式错误。开发者需要确保格式字符串与变量类型匹配,否则会导致未定义行为。
  • cin 和 cout 会根据变量类型自动处理输入输出,避免格式化错误。相对 scanf 和printf 而且,C++的 cin 和 cout 更加易用。
  • scanf 和 printf:格式化输出更精确直观,特别适合复杂格式的输入输出,比如:在要求指定格式输出的时候,printf 函数就比 cout 更加方便和灵活。
#include<cstdio>#include<iostream>usingnamespace std;intmain(){float a =3.50;double d =16.50; cout <<"cout: "<<a <<" "<< d <<endl;printf("printf: %f %lf\n", a, d);return0;}

输入结果如下:

在这里插入图片描述
区别:cout 默认不会输出六位小数,自动忽略小数点后多余的 0,printf 函数打印浮点
数的时候,小数点默认打印 6 位。cout 在输出的时候不需要指定格式,printf 则需要明确的格式。

3.2 性能差异

3.2.1 案例演示

结论: scanf 和 printf 通常比 cin 和 cout 快。
原因: cin 和 cout 由于要考虑兼容 C 语言的输入和输出,封装实现的更加复杂,通常比 scanf 和 printf 稍慢,但这种差异在大多数应用场景中可以忽略不计。但是在竞赛的题目中,尤其是当输入、输出数据量较大时,使用 cin 和 cout 完成输入输出,经常会出现 Time Limit Exceeded 的情况。而 scanf 和 printf 就不存在类似的问题。下面给大家准备了两个案例。

案例一:数字游戏

代码展示:

  1. 使用 cin 和 cout:
#include<iostream>usingnamespace std;int t, x;intmain(){ cin >> t;while(t--){ cin >> x;int ret =0;while(x){int count =0, high =0;int tmp = x;while(tmp){//计算最右边的1代表的值 int low = tmp &-tmp;//如果low中剩余的1就是最后一个1 //就是最左边的1 if(tmp == low){ high = low;}//去掉最右边的1  tmp -= low; count++;}if(count %2==0){ x -= high;}else{ x ^=1;} ret++;} cout << ret << endl;}return0;}

运行结果展示:

在这里插入图片描述
  1. 使用 scanf 和printf:
#include<iostream>usingnamespace std;int t, x;intmain(){scanf("%d",&t);while(t--){scanf("%d",&x);int ret =0;while(x){int count =0, high =0;int tmp = x;while(tmp){//计算最右边的1代表的值 int low = tmp &-tmp;//如果low中剩余的1就是最后一个1 //就是最左边的1 if(tmp == low){ high = low;}//去掉最右边的1  tmp -= low; count++;}if(count %2==0){ x -= high;}else{ x ^=1;} ret++;}printf("%d\n", ret);}return0;}

运行结果展示:

在这里插入图片描述

案例 2:求第 k 小的数

  1. 使用 cin 和 cout:
#include<iostream>#include<cstdio>#include<algorithm>usingnamespace std;constint N =5000010;int arr[N];intmain(){int n, k; cin >> n >> k;for(int i =0; i < n; i++){ cin >> arr[i];}sort(arr, arr + n); cout << arr[k]<< endl;return0;}

运行结果展示:

在这里插入图片描述
  1. 使用 scanf 和printf:
#include<iostream>#include<cstdio>#include<algorithm>usingnamespace std;constint N =5000010;int arr[N];intmain(){int n, k; cin >> n >> k;for(int i =0; i < n; i++){scanf("%d",&arr[i]);}sort(arr, arr + n); cout << arr[k]<< endl;return0;}

运行结果展示:

在这里插入图片描述

我们可以看到这两个案例中,输入的数据量都比较大,在输入数据的时候如果使用cin,都会出现超时的问题,但是换成是 scanf 的方式就能正确的通过。这就是因为两者性能上的差异导致的。那为什么 cin/cout 的性能要低于 scanf/printf 呢?有什么办法能优化吗?

后面会单独对这个问题进行讲解。

总结一下其实就是 2 个点:

  1. C++ 中为了支持混合使用 cin/cout 和 scanf/printf,C++ 标准库默认会将 cin、cout
    等 C++ 流对象与 stdin、stdout 等 C 标准库的流对象同步在一起。这种同步操作意味着每次使用 cin或cout 时,都会自动刷新 C 标准库的缓冲区,以保 C++ 和 C 的 I/O 是一致的。这就导致了性能的下降。
  2. 在默认情况下,cin 和 cout 之间存在一种绑定关系。这种绑定意味着,每当从 cin 读取数据时,任何之前通过 cout 输出的内容都会被强制刷新到屏幕上。这种绑定也可能导致性能问题,特别是在需要频繁读取大量数据的情况下。

3.2.2 优化方案和演示

通过下面这个案例我们看一下 scanf 和 cin 的差异,然后再看看如何优化。

#include<iostream>#include<ctime>#include<cstdio>usingnamespace std;constint num =10000000;intmain(){int i, x;//freopen是将stdin重定向到文件 //意思是scanf可以文件中读取数据 freopen("data.txt","r",stdin); clock_t t1, t2; t1 =clock();for(i =0; i < num; i++){scanf("%d",&x);} t2 =clock(); cout <<"Runtime of scanf: "<< t2 - t1 <<" ms"<< endl;return0;}

运行演示结果:

在这里插入图片描述
#include<iostream>#include<ctime>#include<cstdio>usingnamespace std;constint num =10000000;intmain(){//freopen是将stdin重定向到文件 //意思是cin可以文件中读取数据 freopen("data.txt","r",stdin);int i, x; clock_t t1, t2; t1 =clock();for(i =0; i < num; i++){ cin >> x;} t2 =clock(); cout <<"Runtime of cin: "<< t2 - t1 <<" ms"<< endl;return0;}

运行演示结果:

在这里插入图片描述

如果对 cin 进行优化呢?

#include<iostream>#include<ctime>#include<cstdio>usingnamespace std;constint num =10000000;intmain(){ ios::sync_with_stdio(false);//取消给C语言输入输出缓冲区的同步  cin.tie(0);//取消了cin和cout的绑定 freopen("data.txt","r",stdin);int i, x; clock_t t1, t2; t1 =clock();for(i =0; i < num; i++){ cin >> x;} t2 =clock(); cout <<"Runtime of cin: "<< t2 - t1 <<" ms"<< endl;return0;}

运行演示结果:

在这里插入图片描述

所以未来我们在使用 scanf / printf 和 cin / cout 抉择的时候,如果要追求性能那就使用 scanf / printf,或者优化版的 cin / cout,如果不追求性能,直接使用 cin / cout 就行。

如果输入的数据量比较小(10^6 以内)的话,用 cin 和 cout 或 scanf 和 printf 都行;但是输入的数据量比较大(10^9 左右)的话,更推荐使用 scanf 和 printf,避免因为输入输出的开销,导致代码超时;在大多数场景下 printf / scanf 和 cin / cout 的使用根据个人习惯进行选择
即可。

这里我们讨论了 scanf/printf 和 cin/cout 性能的差异,我们知道 scanf/printf 是更快
的,其实有一些极端情况下,输入输出的规模非常大的时候,scanf 和 printf 也不能满足的时候,会使用快速读写的方式,这个后期会讲。


结语

掌握输入输出的底层机制和陷阱,才能写出健壮高效的代码。从缓冲策略到格式控制,每个细节都可能成为调试时的关键。

希望这篇文章能给各位朋友对 C/C++ 的输入输出有着不一样的理解。

同时愿诸君能一起共渡重重浪,终见缛彩遥分地,繁光远缀天



Read more

C++之多态

C++之多态

多态 * 什么是多态? * 多态的定义及实现 * 多态的构成条件 * 虚函数 * 虚函数的重写/覆盖 * 关键技术原理 * 最佳实践指南 * 虚函数重写 * 协变 * 析构函数的重写 * override和final关键字 * 纯虚函数和抽象类 * 多态的原理 * 多态是如何实现的 * 1. 虚函数表(vtable) * 虚函数表知识要点 * 2. 虚函数的声明 * 3. 多态的实现过程 * 动态绑定与静态绑定 什么是多态? 多态(Polymorphism)是面向对象编程的三大核心特性之一(封装、继承、多态),源于希腊语"多种形态"。在C++中,它允许我们使用统一的接口处理不同类型的对象,显著提高了代码的灵活性和可扩展性。 核心概念 1. 同一接口,多种形态 不同的对象可以通过相同的方法名调用,但实际执行的逻辑由对象自身的类决定。 2. 解耦调用与实现 调用者只需关注接口(方法名和参数)

By Ne0inhk
【C++】模板编程入门指南:零基础掌握泛型编程核心(初阶)

【C++】模板编程入门指南:零基础掌握泛型编程核心(初阶)

文章目录 * 一、泛型编程 * 二、函数模板 * 1. 函数模板的概念和格式 * 2. 函数模板的原理 * 3. 函数模板的实例化 * 隐式实例化 * 显式实例化 * 三、类模板 一、泛型编程 泛型编程就是编写与类型无关的通用代码,是代码复用的一种手段,模板是泛型编程的基础,可能不太好理解,这里我给大家举一个现实生活中的例子,我们想做很多个草莓形状的橡皮泥玩具,并且这些草莓玩具颜色不同,效果如下: 问题来了,我们该怎么解决这个问题呢?难道拿出不同颜色的橡皮泥开始一个一个捏吗?但是这样的话效率是不是很低呢?所以我们会这样想,既然这些草莓玩具的形状相同,只是颜色不同,我们是不是可以做一个草莓模具,当我们想做一个草莓玩具的时候,就可以将对应颜色的橡皮泥填充模具,最终得到这个草莓,如下: 这样我们有了模具以后,只需要使用对应颜色的橡皮泥就可以批量制作草莓了,非常高效,这就属于泛型编程的思维,大家可能还是感受不到,我们再举一个有关编程的例子,也就是使用C语言实现两个变量的交换,如下: voidSwap(int& x,int&

By Ne0inhk
改造红黑树实现封装 map/set:感受C++ 标准容器的精妙设计与底层实现

改造红黑树实现封装 map/set:感受C++ 标准容器的精妙设计与底层实现

容器map/set的底层是红黑树,这一篇详解红黑树如何封装实现map/set。 1.map/set设计的巧妙之处 map是key/value类型,set是key类型,两个冲突的参数类型,是如何由红黑树封装而成? 暴力思路:两个红黑树,一个kv,一个k。可是这样代码复用率极低,维护成本高。 源码思路:利用 键提取器——仿函数 提取kv、k的key,用一颗红黑树实现map,set C语言一般用函数指针,但是它十分麻烦,C++有了仿函数就很方便 接下来在红黑树基础上封装map和set 2.map和set的实现 2.1map和set的基本框架 + 原红黑树结构变化 map是key、value结构,set是key结构:  既然我们要用一个红黑树封装实现map和set,那传的参数就得通用: 原本是K,V结构,现在,要改成通用的,就用T吧 T根据需要,可选择传pair<K,

By Ne0inhk

MAVLink 通信协议 C++ 开发实战:从环境搭建到飞控通信全解析

前言 MAVLink(Micro Air Vehicle Link)是一款轻量级、低带宽、高可靠性的微小型无人机通信协议,由 PX4 团队主导设计,广泛应用于无人机、无人车、机器人等嵌入式系统的跨设备通信场景。其核心优势在于专为资源受限的硬件(如 MCU、小型嵌入式板卡)优化,采用二进制编码格式,相比 JSON、XML 等文本协议,传输效率提升数倍,同时支持数据校验、重传机制,能在复杂电磁环境下保证通信稳定性。 从 MAVLink 协议核心原理出发,结合 C++ 语言实现完整的通信流程,涵盖协议环境搭建、消息编解码、串口 / 网络通信、数据收发实战等核心内容,所有代码均可直接移植到 Linux/Windows 嵌入式环境,助力快速上手 MAVLink C++ 开发。 一、MAVLink

By Ne0inhk