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\) 次操作,每次操作为以下之一:

  1. 在末尾拼接字符串 \(t\)
  2. \(a\) 位置开始截取长度为 \(b\) 的子串
  3. \(a\) 位置插入字符串 \(t\)
  4. 查找字符串 \(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

在本地编译时,程序使用文件读写;在洛谷提交时,因为没定义 LOCALfreopen 被跳过,使用标准输入输出。

注意:在洛谷提交代码时,一定要去掉或注释掉 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:strlensizeof 混淆

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:stringchar[] 互转

stringchar[]strcpy(cstr, s.c_str());
char[]stringstring s(cstr);

一句话总结:字符串处理 = 字符数组 / string + 遍历每个字符 + ASCII 码运算。遇到字符串题,先想好用什么方式读入(可能含空格吗?),再处理每个字符。

posted @ 2026-07-04 08:57  surprise_ying  阅读(26)  评论(0)    收藏  举报