01入门篇-05字符串
【入门5】字符串与文件操作
8 道题 | 难度 2-4 | 约 90 分钟 | 零基础入门
上节课回顾
📝 【入门4】数组与数据批量存储
| 知识点 | 核心思想 |
|---|---|
| 一维数组 | int a[N]; 批量存储数据,循环遍历 |
| 二维数组 | int a[N][M]; 表格存储,行列表遍历 |
| 标记法 | 用下标代表数据,值代表状态 |
| 数组越界 | 多开空间,避免访问超出范围 |
数组存储的是数字,但程序中经常需要处理文本——姓名、标题、单词等。这就需要用字符串来存储字符序列。
课程导入
字符串本质上是字符的序列。C++ 中处理字符串有两种方式:
| 方式 | 特点 | 适用场景 |
|---|---|---|
char[] 字符数组 |
C 风格,需要手动管理结尾 '\0' |
与 C 代码兼容、底层操作 |
string 类型 |
C++ 风格,自动管理,功能丰富 | 大部分竞赛场景 |
字符串的结束标志 '\0': 无论是字符数组还是 string,字符串都以空字符 '\0'(ASCII 码 0)表示结束。strlen()、cout << 等都是遇到 '\0' 才停止。
本课学习路线: 字符数组 → string 类型 → 字符串处理函数 → 文件操作
本课知识地图
| 知识模块 | 核心概念 | 对应例题 |
|---|---|---|
| 字符数组 | char[] · strlen() · 字符大小写转换 |
例1 P5733 → 例2 P1914 → 例3 P1125 → 例4 P1957 |
string 类型 |
string · .substr() · .find() · .insert() |
例5 P5015 → 例6 P5734 → 例7 P1308 |
| 文件操作与重定向 | freopen · 文件输入输出 |
例8(知识点) |
知识点详解
1. 字符数组
// 定义字符数组
char s[100]; // 可存 99 个字符 + 结尾 '\0'
char s[] = "Hello"; // 自动分配 6 个空间(含 '\0')
// 读入
cin >> s; // 读入到空白字符(空格、换行)停止
gets(s); // 读入一整行(不含换行符)
fgets(s, sizeof(s), stdin); // 安全读入一行
// 输出
cout << s; // 输出直到 '\0'
printf("%s", s); // C 格式输出
// 常用函数(需要 <cstring>)
strlen(s); // 返回字符串长度(不含 '\0')
strcpy(s1, s2); // 把 s2 复制到 s1
strcat(s1, s2); // 把 s2 连接到 s1 末尾
strcmp(s1, s2); // 比较,相等返回 0
2. 字符与 ASCII 码
每个字符对应一个 ASCII 码(整数),常见范围:
| 范围 | 含义 | 十进制 |
|---|---|---|
'0' ~ '9' |
数字 | 48 ~ 57 |
'A' ~ 'Z' |
大写字母 | 65 ~ 90 |
'a' ~ 'z' |
小写字母 | 97 ~ 122 |
大小写转换:
char c = 'a';
c = c - 'a' + 'A'; // 小写转大写:'a' → 'A'
c = c - 'A' + 'a'; // 大写转小写:'A' → 'a'
c = toupper(c); // 函数版:转大写(需要 <cctype>)
c = tolower(c); // 函数版:转小写
3. string 类型
C++ 的 string 类型(需要 <string> 或 <bits/stdc++.h>)提供了丰富的字符串操作。
可以把 string 想象成一排带编号的格子,每个格子里有一个字符:
| 下标 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 字符 | H | e | l | l | o |
s.size() = 5
基础操作
string s; // 定义空字符串 —— 就像拿一个空的铅笔盒
string s = "Hello"; // 定义并初始化 —— 铅笔盒里装好了 5 支笔
// 读入
cin >> s; // 读入到空白字符(空格/换行)就停止
getline(cin, s); // 读入一整行(含空格),像读一整句话
获取长度
s.size(); // 返回字符串长度
s.length(); // 同上,效果完全一样
// 举例:s = "Hello"
// s.size() = 5,因为有 5 个字符
string s = "Hello"
下标: 0 1 2 3 4
字符: [ H ] [ e ] [ l ] [ l ] [ o ]
←────── 5 个字符 ──────→
cout << s.size(); // 输出 5
访问字符
s[i]; // 访问第 i 个字符(下标从 0 开始)
// 举例:s = "Hello"
// s[0] = 'H' 第 1 个字符
// s[1] = 'e' 第 2 个字符
// s[4] = 'o' 第 5 个字符
s[0] s[1] s[2] s[3] s[4]
↓ ↓ ↓ ↓ ↓
[ H ] [ e ] [ l ] [ l ] [ o ]
↑
s[0] —— 第一个字符是 'H'
拼接与追加
s += "World"; // 把 "World" 拼接到 s 末尾
s.append("!"); // 追加 "!" 到末尾(和 += 效果一样)
// 举例:s = "Hello"
// s += "World"; → s = "HelloWorld"
拼接前:s = "Hello"
字符: [ H ][ e ][ l ][ l ][ o ]
执行 s += "World"; 相当于在后面接上新内容:
字符: [ H ][ e ][ l ][ l ][ o ][ W ][ o ][ r ][ l ][ d ]
←──── 原来 ────→←──── 新拼接的 "World" ────→
截取子串 .substr()
s.substr(pos, len); // 从位置 pos 开始,截取 len 个字符
s.substr(pos); // 从位置 pos 开始,一直截取到字符串末尾
// 举例:s = "Hello World"
// s.substr(0, 5) = "Hello" 从第 0 位开始取 5 个
// s.substr(6, 5) = "World" 从第 6 位开始取 5 个
// s.substr(6) = "World" 从第 6 位一直取到末尾(只传一个参数)
// s.substr(0) = "Hello World" 从第 0 位取到末尾(相当于复制整个字符串)
s = "Hello World"
下标:0 1 2 3 4 5 6 7 8 9 10
H e l l o W o r l d
←─────→ ←─────────→
s.substr(0,5) s.substr(6)
"Hello" "World"
←──────────────────────→
s.substr(0) = "Hello World"(取到末尾)
s = "Hello World"
下标: 0 1 2 3 4 5 6 7 8 9 10
字符:[ H ][ e ][ l ][ l ][ o ][ ][ W ][ o ][ r ][ l ][ d ]
↑─────────↑ ↑───────────────────────────────↑
substr(0,5) = "Hello" substr(6) ——不传 len,取到末尾
= "World"
查找 .find()
s.find(t); // 在 s 中查找子串 t,返回第一次出现的位置
// 如果找不到,返回 string::npos
// 举例:s = "Hello World"
// s.find("World") = 6 从第 6 位开始是 "World"
// s.find("o") = 4 "o" 第一次出现在第 4 位
// s.find("xyz") = npos 找不到
s = "Hello World"
下标:0 1 2 3 4 5 6 7 8 9 10
H e l l o W o r l d
↑
s.find("World") = 6
——从第 6 个位置开始找到了 "World"
找不到时返回 string::npos(一个很大的数)
判断方式:if (s.find(t) == string::npos) 说明没找到
插入 .insert()
s.insert(pos, t); // 在位置 pos 前插入字符串 t
// 举例:s = "Hello"
// s.insert(3, "ABC"); → s = "HelABClo"
插入前:s = "Hello"
字符:[ H ][ e ][ l ][ l ][ o ]
↑
在 s[3] 前面插入 "ABC"
插入后:s = "HelABClo"
字符:[ H ][ e ][ l ][ A ][ B ][ C ][ l ][ o ]
↑←──插入 "ABC"──→↑
原来的 'l' 和 'o' 被挤到了右边
删除 .erase()
s.erase(pos, len); // 从位置 pos 开始,删除 len 个字符
// 举例:s = "Hello World"
// s.erase(5, 6); → s = "Hello"
// 从第 5 位开始删 6 个(删掉 " World")
删除前:s = "Hello World"
删除前:s = "Hello World"
字符:[ H ][ e ][ l ][ l ][ o ][ ][ W ][ o ][ r ][ l ][ d ]
↑─── 删除这 6 个字符 ──→
执行 s.erase(5, 6); ——从第 5 位开始删 6 个
删除后:s = "Hello"
字符:[ H ][ e ][ l ][ l ][ o ]
4. 文件操作与重定向
竞赛中默认使用标准输入输出(键盘/屏幕)。但有时需要用文件进行本地调试。
// 文件重定向
freopen("input.txt", "r", stdin); // 从文件读入
freopen("output.txt", "w", stdout); // 输出到文件
// 之后 cin/cout 自动读写文件
注意:在洛谷等在线评测系统中,不需要在代码中加入
freopen,系统会自动重定向。文件操作通常只在本地调试时使用。
例题精讲
例 1:P5733 【深基6.例1】自动修正
知识点: 字符数组 — 大小写转换
题意
输入一个字符串,将其中的小写字母全部转为大写后输出。
思路详解
遍历字符串的每个字符,如果是小写字母('a'~'z'),就转为大写字母。
大小写转换公式:小写 = 大写 - 'A' + 'a' 或 大写 = 小写 - 'a' + 'A'。
AC 代码
#include<bits/stdc++.h> // 头文件:包含 C++ 所有标准库(竞赛常用写法)
using namespace std; // 使用标准命名空间:让 cout、cin 等可以直接写
char a[105]; // 定义字符数组 a,最多存 105 个字符
// 字符数组用来存储字符串,每个位置存一个字符
int main() // 主函数——程序从这里开始执行
{
cin >> a; // 从键盘读入一个字符串,存入字符数组 a
// cin 会自动在末尾加 \0 表示字符串结束
// 遍历字符串中的每个字符,a[i] 等价于 a[i] != '\0'
// \0 是字符串结束标记(ASCII 码为 0),遇到它就说明字符串结束了
for (int i = 0; a[i]; i++)
{
// 判断是否是小写字母(小写字母 a~z 的 ASCII 码是 97~122)
if (a[i] >= 'a' && a[i] <= 'z')
{
// 小写转大写公式:小写字母 - 'a' + 'A' = 大写字母
// 原理:'a'=97, 'A'=65,大小写差 32
// 例如 'b'(98) → 98-97+65 = 66 = 'B'
a[i] = a[i] - 'a' + 'A';
}
}
cout << a << endl; // 输出转换后的字符串
return 0; // 返回 0,告诉操作系统"程序正常结束"
}
例 2:P1914 小书童——凯撒密码
知识点: 字符数组 — 字符移位
题意
给定一个位移 \(n\) 和一个字符串,将字符串中每个字母向后移动 \(n\) 位(循环,z 后回到 a),输出加密后的结果。
思路详解
每个字符 c 向后移 \(n\) 位:c = (c - 'a' + n) % 26 + 'a'。
关键是要处理循环:超出 'z' 时要绕回 'a'。
// 方法一:判断是否超出
if (c + n > 'z') c = c + n - 26;
else c = c + n;
// 方法二:取模(更简洁)
c = (c - 'a' + n) % 26 + 'a';
AC 代码
#include<bits/stdc++.h> // 头文件:包含 C++ 所有标准库
using namespace std; // 使用标准命名空间
int main() // 主函数——程序从这里开始执行
{
int n; // n:每个字母向后移动的位数
char a[55]; // 字符数组 a:用来存储输入的字符串(最多 55 个字符)
cin >> n >> a; // 先读入位移 n,再读入需要加密的字符串
// 例如输入 "3 Hello" 后,n=3,a="Hello"
// 遍历字符串的每个字符,a[i] 相当于 a[i] != '\0'
// \0 是字符串结束标记,遇到它说明字符串结束了
for (int i = 0; a[i]; i++)
{
// 凯撒密码加密公式:
// a[i] - 'a' → 把字母转成 0~25 的数字('a'=0, 'b'=1, ...)
// + n → 向后移动 n 位
// % 26 → 如果超出 'z',绕回 'a'(比如 'z'+1='a')
// + 'a' → 把数字转回字母
a[i] = (a[i] - 'a' + n) % 26 + 'a';
// 举例:字母 'y'(24),n=3
// (24 + 3) % 26 + 'a' = 27 % 26 + 'a' = 1 + 'a' = 'b'
}
cout << a << endl; // 输出加密后的字符串
return 0; // 返回 0,程序正常结束
}
例 3:P1125 [NOIP2008 提高组] 笨小猴
知识点: 字符数组 —
strlen()、strcpy()、字符数组初始化
题意
输入一个单词,统计每个字母出现的次数,找出出现次数最多的和最少的(只统计出现过的字母)。如果最大值减最小值是质数,输出 Lucky Word 和差值,否则输出 No Answer 和 0。
思路详解
1. 统计字母出现次数
用一个大小为 26 的数组 a[26],a[0] 对应 'a',a[1] 对应 'b'……
a[s[i] - 'a']++; // 字符转下标
2. 找最大最小值(跳过没出现过的)
if (a[i] == 0) continue; // 没出现过的字母不统计
3. 判断质数
AC 代码
#include<bits/stdc++.h> // 头文件:包含 C++ 所有标准库
using namespace std; // 使用标准命名空间
int a[26]; // 计数数组:a[0] 存字母 'a' 出现次数,a[1] 存 'b' 出现次数……
// 数组大小 26 正好对应 26 个英文字母
// 判断质数的函数(x 是质数返回 1,否则返回 0)
int check(int x)
{
if (x < 2) return 0; // 0 和 1 都不是质数
// 从 2 遍历到 sqrt(x)(i*i <= x),检查 x 能否被整除
for (int i = 2; i * i <= x; i++)
if (x % i == 0) return 0; // 如果能被某个数整除,说明不是质数
return 1; // 循环结束都没被整除,说明是质数
}
int main() // 主函数入口
{
string s; // 定义 string 变量 s,用来存储输入的单词
cin >> s; // 读入单词字符串
// 遍历单词的每个字符
for (int i = 0; i < s.size(); i++)
{
// s[i] 是当前字符,s[i] - 'a' 把字母转成下标('a'→0, 'b'→1, ...)
// 例如 s[i]='c',则 'c'-'a'=2,所以 a[2]++(字母 c 的计数加 1)
a[s[i] - 'a']++;
}
int maxn = 0; // maxn:出现次数最多的字母的次数(初始化为 0)
int minn = 1e9; // minn:出现次数最少的字母的次数(初始化为很大)
// 遍历 26 个字母的计数数组
for (int i = 0; i < 26; i++)
{
if (a[i] == 0) continue; // 跳过没出现过的字母(计数为 0)
if (a[i] > maxn) maxn = a[i]; // 更新最大值
if (a[i] < minn) minn = a[i]; // 更新最小值
}
int cha = maxn - minn; // 计算极差(最多出现次数 - 最少出现次数)
// 判断极差是否为质数
if (check(cha))
cout << "Lucky Word" << endl << cha << endl; // 是质数:输出 "Lucky Word" 和极差
else
cout << "No Answer" << endl << 0 << endl; // 不是质数:输出 "No Answer" 和 0
return 0; // 程序正常结束
}
例 4:P1957 口算练习题
知识点: 字符数组 —
fgets()读入行、sscanf()格式化读取、sprintf()格式化输出
题意
输入 \(n\) 行,每行可能是"a x y"(加法)、"b x y"(减法)、"c x y"(乘法),或者直接是"x y"(延续上一次的运算符)。输出算式和结果,以及算式字符串的长度。
思路详解
1. 读入一整行
用 fgets 读入一行字符串(包含空格)。
2. 解析字符串
用 sscanf 从字符串中按格式提取数据。
// 如果第一个字符是字母,说明指定了操作
if (s[0] >= 'a' && s[0] <= 'c')
sscanf(s, "%c %d %d", &op, &x, &y);
else // 否则延续上一操作
sscanf(s, "%d %d", &x, &y);
3. 输出结果
用 sprintf 将算式格式化输出到字符串。
AC 代码
#include<bits/stdc++.h> // 头文件:包含 C++ 所有标准库
using namespace std; // 使用标准命名空间
int main() // 主函数入口
{
char s[105]; // s:用来存储读入的每一行字符串(最多 100 个字符)
char t[105]; // t:用来存储格式化后的算式字符串(如 "12+34=46")
int n; // n:数据的组数(一共有几行输入)
scanf("%d", &n); // 读入数据组数 n
fgets(s, sizeof(s), stdin); // 读掉第一行末尾的换行符
// 因为 scanf 读 n 后,缓冲区里还有个 \n
// 不读掉的话,下面的 fgets 会直接读到这个 \n 而跳过
char op = 'a'; // 当前运算类型:'a'=加法 'b'=减法 'c'=乘法,默认加法
int x, y; // 两个操作数
// 循环处理每组数据
for (int i = 1; i <= n; i++)
{
fgets(s, sizeof(s), stdin); // 读入一整行字符串(包含空格)
// 输入可能是 "a 12 34" 或 "12 34"(延续上一种运算)
// 判断这一行的第一个字符是否为字母
// 如果是 'a'/'b'/'c',说明这一行指定了运算类型
if (s[0] >= 'a' && s[0] <= 'c')
// sscanf 表示从字符串 s 中按格式提取数据
// 相当于从字符串 s 中"读"出运算符和两个数字
sscanf(s, "%c %d %d", &op, &x, &y);
else
// 第一个字符不是字母,说明延续上次的运算类型
// 只需提取两个数字
sscanf(s, "%d %d", &x, &y);
// 根据运算类型计算结果,用 sprintf 把算式和结果写入字符串 t
// sprintf 和 printf 功能一样,但 printf 输出到屏幕,sprintf 输出到字符串
if (op == 'a')
sprintf(t, "%d+%d=%d", x, y, x + y); // 加法
else if (op == 'b')
sprintf(t, "%d-%d=%d", x, y, x - y); // 减法
else
sprintf(t, "%d*%d=%d", x, y, x * y); // 乘法
// 输出算式字符串 t,以及 t 的长度(strlen)
// "%s" 输出字符串,"\n" 换行,"%d" 输出长度
printf("%s\n%d\n", t, (int)strlen(t));
}
return 0; // 程序正常结束
}
例 5:P5015 [NOIP2018 普及组] 标题统计
知识点:
string类型 — 定义、读入、长度
题意
输入一行字符串(可能包含空格),统计其中字符的个数(空格和换行符不算)。
思路详解
1. 读入整行(含空格)
cin >> 读入到空白字符就会停,而 getline(cin, s) 能读入一整行。
2. 遍历并排除空格
对每个字符,如果不是空格就计数。
AC 代码
#include<bits/stdc++.h> // 头文件:包含 C++ 所有标准库
using namespace std; // 使用标准命名空间
int main() // 主函数入口
{
string s; // 定义 string 类型变量 s,用来存储输入的标题字符串
// getline 可以读入一整行,包括空格(cin >> 遇到空格就会停止)
// 输入的标题可能包含空格,所以必须用 getline
getline(cin, s); // 从键盘读入一整行字符串,存入 s
int ans = 0; // ans:字符计数器,初始为 0
// s.size() 返回字符串的长度(字符个数)
// 遍历字符串的每个字符,下标从 0 到 s.size()-1
for (int i = 0; i < s.size(); i++)
{
// 如果这个字符不是空格,就计数器加 1
// 题目要求:空格和换行符不算字符
if (s[i] != ' ') ans++;
}
cout << ans << endl; // 输出字符总数
return 0; // 程序正常结束
}
例 6:P5734 【深基6.例6】文字处理软件
知识点:
string类型 —substr()、insert()、find()等操作
题意
初始有一个字符串 \(s\),有 \(n\) 次操作,每次操作为以下之一:
- 在末尾拼接字符串 \(t\)
- 从 \(a\) 位置开始截取长度为 \(b\) 的子串
- 在 \(a\) 位置插入字符串 \(t\)
- 查找字符串 \(t\) 在 \(s\) 中第一次出现的位置
思路详解
直接使用 string 提供的成员函数即可。
| 操作 | 代码 | 说明 |
|---|---|---|
| 拼接 | s = s + t |
把 t 拼到 s 末尾 |
| 截取 | s.substr(a, b) |
从 a 开始取 b 个字符 |
| 插入 | s.insert(a, t) |
在 a 位置前插入 t |
| 查找 | s.find(t) |
返回位置,找不到返回 string::npos |
AC 代码
#include<bits/stdc++.h> // 头文件:包含 C++ 所有标准库
using namespace std; // 使用标准命名空间
int main() // 主函数入口
{
int n; // n:操作次数
string s, t; // s:初始字符串,t:操作中要拼接/插入/查找的字符串
cin >> n >> s; // 读入操作次数 n 和初始字符串 s
// 依次处理 n 次操作
for (int i = 1; i <= n; i++)
{
int op; // op:操作类型(1=拼接,2=截取,3=插入,4=查找)
cin >> op; // 读入操作类型
if (op == 1) // 操作 1:在 s 末尾拼接字符串 t
{
cin >> t; // 读入要拼接的字符串 t
s = s + t; // 把 t 拼接到 s 末尾(s += t; 也一样)
cout << s << endl; // 输出当前 s
}
else if (op == 2) // 操作 2:从位置 a 开始截取 b 个字符
{
int a, b; // a=起始位置,b=截取长度
cin >> a >> b; // 读入 a 和 b
s = s.substr(a, b); // 截取子串并赋给 s
cout << s << endl; // 输出当前 s
}
else if (op == 3) // 操作 3:在位置 a 前插入字符串 t
{
int a; // a=插入位置
cin >> a >> t; // 读入插入位置 a 和要插入的字符串 t
s = s.insert(a, t); // 在 a 位置前插入 t,结果赋给 s
cout << s << endl; // 输出当前 s
}
else // 操作 4:查找字符串 t 在 s 中第一次出现的位置
{
cin >> t; // 读入要查找的字符串 t
// s.find(t) 返回 t 第一次出现的位置(下标)
// 如果找不到,返回 string::npos(一个很大的数,等于 -1)
// (int) 强制类型转换,把 npos 转成整数输出
cout << (int)s.find(t) << endl;
}
}
return 0; // 程序正常结束
}
例 7:P1308 [NOIP2011 普及组] 统计单词数
知识点:
string类型 — 大小写转换、单词拆分
题意
输入一个单词和一段文章,统计该单词在文章中出现的次数和第一次出现的位置(不区分大小写)。如果没出现,输出 -1。
思路详解
1. 全部转小写
不区分大小写,把单词和文章都转为小写。
for (int i = 0; i < s.size(); i++) s[i] = tolower(s[i]);
2. 按空格拆分单词
在文章末尾加一个空格,方便统一处理。然后依次读取每个单词,和关键词比较。
AC 代码
#include<bits/stdc++.h> // 头文件:包含 C++ 所有标准库
using namespace std; // 使用标准命名空间
int main() // 主函数入口
{
string s; // s:要查找的单词(关键词)
string t; // t:文章内容
cin >> s; // 读入要查找的单词(遇到空格/换行停止)
getline(cin, t); // getline 会读掉上一行 cin 留下的换行符
getline(cin, t); // 读入整篇文章(包含空格,用 getline)
// 将关键词全部转为小写(不区分大小写)
// tolower(c) 是 cctype 库的函数,将字符 c 转为小写
// 如果 c 是大写字母,返回小写字母;否则返回原字符
for (int i = 0; i < s.size(); i++)
s[i] = tolower(s[i]);
// 将文章也全部转为小写
for (int i = 0; i < t.size(); i++)
t[i] = tolower(t[i]);
t = t + ' '; // 在文章末尾加一个空格,方便统一处理最后一个单词
// 这样每个单词后面都有一个空格,不需要特殊处理末尾
string tmp = ""; // tmp:临时存储当前正在读取的单词
int cnt = 0; // cnt:关键词出现的总次数
int first = -1; // first:关键词第一次出现的位置(下标),-1 表示还没找到
// 逐个字符遍历文章
for (int i = 0; i < t.size(); i++)
{
if (t[i] == ' ') // 遇到空格,说明一个单词已经结束
{
// 将 tmp 中存储的单词与关键词 s 比较
if (tmp == s) // 如果相等,说明找到了一个匹配
{
cnt++; // 出现次数加 1
if (first == -1) // 如果是第一次找到
first = i - tmp.size(); // 记录第一次出现的位置(当前下标 - 单词长度)
}
tmp = ""; // 清空 tmp,准备读取下一个单词
}
else
{
tmp += t[i]; // 不是空格,说明还在单词内部,把当前字符拼接到 tmp
}
}
// 输出结果
if (cnt == 0) // 一次都没出现
cout << -1 << endl; // 输出 -1
else // 出现了
cout << cnt << " " << first << endl; // 输出出现次数和第一次出现的位置
return 0; // 程序正常结束
}
例 8:文件操作与重定向
知识点:
freopen重定向输入输出
教学讲解
为什么需要文件操作?
在本地调试时,如果每次都要手动输入大量测试数据,非常麻烦。用文件操作可以将测试数据保存在文件中,程序自动读取。
使用方法:
#include<bits/stdc++.h>
using namespace std;
int main()
{
freopen("input.txt", "r", stdin); // 从文件读入
freopen("output.txt", "w", stdout); // 输出到文件
// 之后的 cin/cout 都会自动读写文件
int a, b;
cin >> a >> b; // 从 input.txt 读入
cout << a + b << endl; // 写入 output.txt
fclose(stdin); // 关闭文件(可选)
fclose(stdout);
return 0;
}
常用模式:
模式 "r" |
含义 |
|---|---|
"r" |
读(read),文件必须存在 |
"w" |
写(write),文件不存在则创建,存在则覆盖 |
"a" |
追加(append),在文件末尾添加 |
调试小技巧:
#ifndef LOCAL
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
#endif
在本地编译时,程序使用文件读写;在洛谷提交时,因为没定义 LOCAL,freopen 被跳过,使用标准输入输出。
注意:在洛谷提交代码时,一定要去掉或注释掉 freopen! 在线评测系统会自动处理输入输出重定向。
常见错误与坑点
错误 1:忘记 '\0' 结束符
char s[5] = {'H', 'e', 'l', 'l', 'o'};
cout << s; // 不会停止!因为没有 '\0',会一直输出直到遇到内存中的 '\0'
解决:确保字符数组以 '\0' 结尾:char s[] = "Hello";(自动加 '\0')。
错误 2:cin >> 读不了空格
cin >> s; // 输入 "Hello World" 只会读到 "Hello"
getline(cin, s); // 才能读到 "Hello World"
错误 3:getline 前的换行问题
int n;
cin >> n; // 读入整数,换行符留在缓冲区
getline(cin, s); // 直接读到了换行符!s 为空
解决:在 getline 前加一个 cin.get() 或 getchar() 读掉换行符。
错误 4:string::npos 的比较
if (s.find(t) == -1) // ❌ 虽然可以,但不规范
if (s.find(t) == string::npos) // ✅ 正确
错误 5:strlen 与 sizeof 混淆
char s[100] = "Hello";
strlen(s); // = 5,字符串的实际长度
sizeof(s); // = 100,数组的容量
题单完整题解(8 题)
P5733 自动修正 ★ — 字符数组入门
题意:小写字母转大写。
核心技巧:ASCII 码运算 c - 'a' + 'A'。
时间 \(O(n)\),空间 \(O(n)\)
AC 代码见例 1。
P1914 小书童——凯撒密码 ★ — 字符移位
题意:字母向后移 n 位,循环。
核心技巧:c = (c - 'a' + n) % 26 + 'a'。
时间 \(O(n)\),空间 \(O(n)\)
AC 代码见例 2。
P1125 笨小猴 ★★★ — 统计字母
题意:找出现次数的极差,判断质数。
核心技巧:a[s[i]-'a']++ 统计字母,注意跳过未出现的字母。
时间 \(O(n)\),空间 \(O(1)\)
AC 代码见例 3。
P1957 口算练习题 ★★★ — 格式化读写
题意:解析算式并输出结果和长度。
核心技巧:fgets 读行 → sscanf 解析 → sprintf 格式化输出。
时间 \(O(n)\),空间 \(O(1)\)
AC 代码见例 4。
P5015 标题统计 ★ — `string` 入门
题意:统计标题字符数(不含空格)。
核心技巧:getline 读入整行,排除空格。
时间 \(O(n)\),空间 \(O(n)\)
AC 代码见例 5。
P5734 文字处理软件 ★★ — `string` 操作
题意:字符串的拼接、截取、插入、查找。
核心技巧:s + t、.substr()、.insert()、.find()。
时间 \(O(n)\),空间 \(O(n)\)
AC 代码见例 6。
P1308 统计单词数 ★★★ — 单词拆分
题意:不区分大小写统计单词出现次数和位置。
核心技巧:转小写 + 按空格拆分单词 + 逐一比较。
时间 \(O(n)\),空间 \(O(n)\)
AC 代码见例 7。
例 8 知识点 — 文件操作与重定向
核心知识点:freopen("file", "r", stdin) / freopen("file", "w", stdout)。
注意:在线评测系统不需要文件操作,仅本地调试使用。
总结与 OI Trick 清单
一、字符串核心操作
| 操作 | char[] 方式 |
string 方式 |
|---|---|---|
| 定义 | char s[100]; |
string s; |
| 读入(含空格) | fgets(s,100,stdin) |
getline(cin, s) |
| 长度 | strlen(s) |
s.size() |
| 拼接 | strcat(s, t) |
s = s + t |
| 查找 | strstr(s, t) |
s.find(t) |
二、字符处理常用公式
| 转换 | 公式 |
|---|---|
| 大写转小写 | c = c - 'A' + 'a' 或 c = tolower(c) |
| 小写转大写 | c = c - 'a' + 'A' 或 c = toupper(c) |
| 数字转整数 | int x = c - '0' |
| 整数转字符 | char c = x + '0' |
| 字母转下标 | int idx = c - 'a'(0~25) |
三、可复用的 OI Trick
Trick 1:用 '\0' 判断字符串结束(P5733, P1914)
for (int i = 0; a[i]; i++)——当a[i]为'\0'(ASCII 0)时循环结束。比strlen更高效。
Trick 2:字母统计数组(P1125)
int cnt[26] = {0}; cnt[s[i] - 'a']++;——下标 0~25 对应 26 个字母。
Trick 3:循环移位用取模(P1914)
c = (c - 'a' + n) % 26 + 'a';——避免if判断边界。
Trick 4:末尾加分隔符统一处理(P1308)
t = t + ' ';——在末尾加空格,这样最后一个单词也能被统一处理。
Trick 5:getline 前的换行处理
cin >> n;后用getline(cin, s)会读到换行符。在中间加cin.get()或getchar()消耗掉换行符。
Trick 6:string 和 char[] 互转
string→char[]:strcpy(cstr, s.c_str());
char[]→string:string s(cstr);
一句话总结:字符串处理 = 字符数组 /
string+ 遍历每个字符 + ASCII 码运算。遇到字符串题,先想好用什么方式读入(可能含空格吗?),再处理每个字符。
浙公网安备 33010602011771号