ACM 一个月学习计划 · DAY 08

Day 8 · 字符与字符串基础

从「数字的世界」走进「文字的世界」——顺便拆掉新手最常踩的那颗地雷:getline

今日目标:能熟练读入、遍历、比较字符串,会做字符判断与大小写转换
预计用时:约 3 小时(含做题)  难度:入门 ~ 普及-
题目来源:洛谷 P5015 / P5733 / P1914 / P1765
← Day 07 · 二维数组 + 第一周复盘

⏱今日时间安排

0 - 20 min字符与 ASCII 的关系(第 1 节)
20 - 50 mincin >> 与 getline 的区别(第 2 节)——今天最大的坑,一定亲手敲一遍
50 - 70 min遍历字符串、字符判断、大小写转换(第 3-4 节)
70 - 150 min做 4 道题,每题限时 30 分钟(前两题各 15 分钟就够)
150 - 180 min对照本文题解补题 + 写笔记 + 做自测
💡 昨天内容的衔接

昨天 P5730 显示屏里出现过一句 pat[s[i] - '0'][r],P5732 里也把数字当字符串读过。
那两个操作其实已经踩进了「字符」的世界,只是当时没细讲。今天把它讲透。
另外 Day 7 练的「按位处理」(把一串东西拆成一个个元素逐个处理)今天会以新形式出现—— 那时拆的是数字的十进制位,今天拆的是字符串里的字符。

1字符其实也是数字:ASCII 码

先接受一个有点反直觉的事实

电脑里存不下「字母 A」这种东西,它只认识数字。所以每个字符都被编了一个号, 这个编号表就叫 ASCII 码表。

比如字母 'A' 的编号是 65,'a' 是 97。 你写 'A' 的时候,电脑看到的其实就是 65。

🎯 由这一点推出的全部结论

既然字符本质上就是整数,那么:
· 字符可以直接比较大小(比的是编号)
· 字符可以做加减法('a' + 1 就是 'b')
· 字符可以直接当数组下标(Day 6 学过的那种桶)

必背的 6 个编号

字符ASCII 码怎么记
'0'48数字字符从 48 开始
'9'5748 + 9,所以 '9' - '0' = 9
'A'65大写字母从 65 开始
'Z'9065 + 25,一共 26 个
'a'97小写字母从 97 开始
'z'12297 + 25
' '(空格)32比所有可见字符都小
💡 真正要背的其实只有 3 件事

① 三组字符各自连续。'0'~'9' 连续、'A'~'Z' 连续、'a'~'z' 连续。 所以「减去起点」就能得到 0 开始的编号:c - '0'、c - 'A'、c - 'a'。

② 大写的编号比小写小。97 − 65 = 32。所以 'a' - 'A' = 32, 大写变小写是 +32,小写变大写是 -32。

③ 它们三组之间是不连续的。'9' 后面紧接着的是 ':' 而不是 'A'。 所以判断字符类型不要靠比大小写死区间,用下面第 4 节的函数最稳。

大小写转换的两种写法

写法代码特点
算术写法c = c - 'a' + 'A';完全依赖「小写比大写大 32」,一眼看不出意图
函数写法c = toupper(c);见名知意,非字母自动不变,推荐

两种都对,但竞赛里建议用函数——少一个「32 是哪来的」的疑问,也少一次手滑。

2今天最大的坑:cin >> 和 getline

两种读入方式,行为完全不同

写法遇到空格 / 制表符会怎样遇到换行会怎样适合读什么
cin >> s停住,只读走前面那一截留在缓冲区里,不读走不带空格的单词 / 号码
getline(cin, s)连空格一起读进去读走并丢掉,作为结束标志含空格的整行句子
🎯 一句话理解区别

cin >> 是「挑一个词」——它会跳过前面的空白,读到下一个空白就停,并且把那个空白留在原地。
getline 是「抄一整行」——它从当前位置一直抄到换行符为止,然后把换行符吃掉。

于是就有了这个经典 bug

假设输入长这样:

5
hello world

你想先读数字 5,再读一整行 "hello world"。很自然地写成:

int n;
string s;
cin >> n;                // 读走 5
getline(cin, s);         // ❌ 你可能以为读到了 "hello world"

实际结果是:s 是空串,长度 0。

为什么?把缓冲区画出来就懂了

步骤输入缓冲区里有什么s 的值
一开始5\nhello world\n—
执行 cin >> n 后\nhello world\n(5 被读走,换行符留下了)—
执行 getline 后hello world\n""(空)

因为 getline 一上来就碰到了那个残留的换行符,它认为「这一行已经结束了」, 于是读到一个空串就返回了。"hello world" 那一行根本还没被碰到。

我实际编译跑了一遍,证据如下

🚫 错误写法
#include <iostream>
#include <string>
using namespace std;

int main(){
    int n;
    string s;
    cin >> n;
    getline(cin, s);          // ❌ 读到的其实是上一行残留的换行符
    cout << "n=" << n << " len=" << s.size() << endl;
    return 0;
}

输入 5 换行 hello world,实测输出:

n=5 len=0

len=0 —— 字符串是空的,"hello world" 根本没被读进来。

✅ 正确写法:加一句 cin.ignore()
#include <iostream>
#include <string>
using namespace std;

int main(){
    int n;
    string s;
    cin >> n;
    cin.ignore();             // ★ 手动吃掉上一行剩下的一个字符(也就是换行)
    getline(cin, s);
    cout << "n=" << n << " s=[" << s << "]" << endl;
    return 0;
}

同样输入 5 换行 hello world,实测输出:

n=5 s=[hello world]

读对了。

💡 更省心的办法:干脆别混用

如果那一行不含空格,那就用 cin >> s 读到底,问题自然消失(今天的 P5733、P1914 就是这样)。
用 getline 的前面不要出现 cin >>,是最省心的策略。

⚠️ 还有两个和 getline 有关的小坑

坑 A:只读一行够不够?本题 P5015 只有一行。但如果题目说「若干行」,你要写成 while(getline(cin, s)) { ... } 一直读到文件结束(EOF)。

坑 B:Window 下的 \r。换行在不同系统里可能是 \n(Linux) 或 \r\n(Windows)。getline 只吃掉 \n, 如果输入文件是 Windows 格式,s 末尾就会多一个看不见的 '\r', 统计字符数时会多算 1。洛谷用的是 Linux 换行,本地测完记得心里有数。 (今天的 P5015 只统计「不是空格的字符」,就是用这个思路做的。)

3遍历字符串:三种写法,用哪种都行

字符串可以理解成「一堆字符排成的一排」,所以 Day 5 学的一维数组那一套完全适用。

string s = "hello";

// 写法 1:下标循环(最通用,推荐新手先用这个)
for(int i = 0; i < (int)s.size(); i++){
    char c = s[i];                 // c 就是第 i 个字符
}

// 写法 2:范围 for(C++11 起,简洁)
for(char c : s){
    // c 依次取 s 里的每个字符(是副本,改它不影响 s)
}

// 写法 3:范围 for + 引用(可以真的改动 s)
for(char &c : s){
    c = toupper(c);                // ★ 加了 & 才是改原件
}
⚠️ for(char c : s) 和 for(char &c : s) 差别很大

· 不加 &:c 只是每个字符的复印件。你在循环里改 c, 原来的 s 一点变化都没有——「改了没效果」是新手最常见的困惑之一。
· 加了 &:c 直接指向字符串里的那个字符,改 c 就是改 s。

拿不准就用写法 1(下标循环),s[i] = ... 一定是改原件,绝不会搞错。

为什么写 (int)s.size() 而不是直接 s.size()

s.size() 返回的是一个无符号整数(size_t)。 你拿 int i 和它比,编译器会给出「有符号与无符号比较」的警告。

写法结果评价
i < s.size()能跑,但可能有警告⚠️ 不太干净
i < (int)s.size()强制转成 int 再比✅ 推荐
int n = s.size();先存进 int 再比✅ 也很常见
💡 这个警告为什么值得在意

无符号数不能表示负数。如果哪天你写了个 i = -1 再和 s.size() 比, -1 会被当成一个超级大的正数,比较结果完全反过来,程序就疯了。 加上 (int) 转换,这一类陷阱就绕开了。

4字符判断与转换:<cctype> 工具箱

C++ 自带一整套判断字符类型的小函数,都在 <cctype> 里。用它们比自己写区间判断更不容易错。

函数作用例子
isdigit(c)是不是数字字符 '0'~'9'isdigit('7') → 真
isalpha(c)是不是字母(大小写都算)isalpha('a') → 真
isupper(c)是不是大写字母isupper('A') → 真
islower(c)是不是小写字母islower('a') → 真
isalnum(c)是不是字母或数字isalnum('!') → 假
isspace(c)是不是空白(空格 / 换行 / Tab)isspace(' ') → 真
toupper(c)转成大写(非字母原样返回)toupper('a') → 'A'
tolower(c)转成小写(非字母原样返回)tolower('A') → 'a'
🎯 它们的返回值是「真 / 假」,不是字符

isdigit 这类函数返回的是「非零表示真、0 表示假」, 所以它们可以直接当 if 的条件用:

if(isdigit(c)) { ... }        // ✅ 直接当条件,最常用
if(isdigit(c) != 0) { ... }   // 也行,但没必要

而 toupper / tolower 返回的是转换后的字符本身,所以要赋值回去。

「只转小写、大写保持不变」的两种写法

// 写法 A:先判断,再转换(意图最清楚)
if(islower(s[i])) s[i] = toupper(s[i]);

// 写法 B:直接转(toupper 对非小写字符原样返回,所以也是对的)
s[i] = toupper(s[i]);

// 写法 C:不用任何函数,纯算术
if(s[i] >= 'a' && s[i] <= 'z') s[i] = s[i] - 'a' + 'A';

三种都能过题。今天的 P5733 我用的是写法 A——多写一个 islower, 但「只处理小写字母」这个意图在代码里写得明明白白。

⚠️ 一个几乎不会碰到、但知道更好的细节

严格来说应该写成 toupper((unsigned char)c)。原因是这些函数接受 int, 如果 char 是有符号的,遇到编号大于 127 的字符时传进去会变成负数,属于未定义行为。
本题输入全是英文和数字(都在 0~127 内),直接写 toupper(c) 完全没问题, 这里只是让你知道有这么回事,以后处理中文或二进制数据时再想起来。

5两个今天会用到的招式

招式一:把字符映射成一个小整数(打表)

P1765 手机那道题,要问「按出字母 c 要按几下键盘」。 这个对应关系是固定的一张表,于是我们用一个数组把答案存起来:

// 用 c - 'a' 把字母映射成 0~25,正好当数组下标(就是 Day 6 的桶思想)
int press[26] = {
    1, 2, 3,        // a b c   在 2 键上
    1, 2, 3,        // d e f   在 3 键上
    1, 2, 3,        // g h i   在 4 键上
    // ...
};

int times = press[c - 'a'];      // ★ 查表,O(1) 得到答案
💡 这叫「预处理 / 打表」

与其在循环里写一堆 if 去判断是哪个键、按几下, 不如一开始就把答案摆好,用的时候直接查。
昨天 P5730 的 0~9 点阵也是同一招。「打表」是竞赛里非常常用的省事技巧。

招式二:让字母「绕圈」(取模)

P1914 凯撒密码要把每个字母往后移 n 位,而且 z 的下一位要绕回 a。 如果直接 c + n,'z' + 1 会变成 '{',跑出字母表了。

正确做法是:先把自己换算成 0~25 的编号,加完再对 26 取余,最后换算回字母。

字母c - 'a'+1 之后% 26'a' + 结果
'q'161717'r'
'w'222323'x'
'e'455'f'
'z'25260'a' ✔
🎯 万能公式(背下来)

s[i] = 'a' + (s[i] - 'a' + n) % 26;

「先转成 0 起的编号 → 加偏移 → 对长度取余 → 再加回起点」这四步, 以后做环形数组、循环队列、环形棋盘都会用到,不只字母能用。

这里的 % 26 和昨天蛇形方阵的 (d + 1) % 4 是完全同一个套路。

6四道例题详解

下面每道题的代码我都用 g++ -std=c++14 -O2 -Wall 实际编译运行过,输出是你看到的真实结果。

P5015 [NOIP 2018 普及组] 标题统计 入门 · getline 读整行

题意:给一个作文标题,问标题里有多少个字符。 标题里可能有大小写字母、数字、空格和换行符,但统计时空格和换行符不算。

⚠️ 数据范围 / 关键约束

输入只有一行。|s| 表示字符串的长度(含空格),题目给的范围是:

· 40% 的数据:1 ≤ |s| ≤ 5,只有数字字符
· 80% 的数据:1 ≤ |s| ≤ 5,只有大小写字母和数字字符
· 100% 的数据:1 ≤ |s| ≤ 5,可能包含大小写字母、数字、空格

也就是说这题的数据非常小(标题最长只有 5 个字符), 难点完全不在数据量上,而在于「有没有把空格正确地排除掉」。

思路

这题只有一句话的思路:用 getline 把整行读进来(这样空格才不会丢),然后数一数有几个字符不是空格。

为什么不能用 cin >> s?因为 cin >> 一遇到空格就停。 样例 2 是 Ca 45,用 cin >> 只能读到 Ca, 答案会算成 2 而不是 4。这就是今天第 2 节那个坑的正确用法:需要空格 → 用 getline。

那「换行符不算」怎么处理?很简单:getline 读出来的字符串里本来就不包含换行符, 它把换行符当结束标志吃掉了。所以我们只要排除空格就行。

样例 1 输入
234
样例 1 输出
3
样例 2 输入
Ca 45
样例 2 输出
4

样例 2 的说明里写得很清楚:有 1 个大写字母、1 个小写字母、2 个数字字符,还有 1 个空格。 空格不算,所以是 4。

完整代码

#include <iostream>
#include <string>
using namespace std;

int main(){
    string s;
    getline(cin, s);                 // ★ 读入一整行,空格也会被读进来

    int ans = 0;
    for(int i = 0; i < (int)s.size(); i++){
        if(s[i] != ' ') ans++;       // ★ 空格不算,其余字符都算
    }
    cout << ans << endl;
    return 0;
}
💡 更稳一点:用 !isspace(s[i])

isspace 会把空格、Tab、换行、回车全都当成「空白」排除掉。 如果担心本地测试文件是 Windows 换行(\r\n)导致多算一个字符, 写成 if(!isspace(s[i])) ans++; 就完全免疫了。

本题洛谷用的是 Linux 换行,所以 != ' ' 已经足够, 两种写法在本题上结果完全一样。

✅ 实测结果

官方样例 1 234 → 输出 3 完全一致
官方样例 2 Ca 45 → 输出 4 完全一致

自造数据 a b c → 输出 3 正确
(3 个字母,2 个空格被排除)

易错点

  • 用 cin >> s → 空格后面的内容全丢,样例 2 会输出 2。
  • 把空格也数进去 → 样例 2 会输出 5。题目明确说空格不算。
  • 只读一个字符(用 cin >> c)→ 标题不止一个字符,直接错。
  • 多读一行:这题只有一行输入。如果写 while(getline(cin, s)) 累加, 在洛谷上其实也能过,但没必要,而且如果行末有多余空行就会出错。
  • 如果前面有 cin >> 读数字,记得先 cin.ignore()(这题没有,但要养成条件反射)。
P5733 【深基6.例1】自动修正 入门 · 大小写转换

题意:办公软件会自动把字母变成大写。输入一个字符串, 把里面所有小写字母变成大写字母,其余字符原样输出。

⚠️ 数据范围 / 关键约束

题目描述里写明:输入字符串长度不超过 100,且不包括空格。 (这道题的「说明/提示」里没有单独的数据范围段落,范围就写在题目描述里。)

「不包括空格」这一条很关键——说明可以直接用 cin >> s 读,不需要 getline。

思路

遍历字符串的每一个字符:如果是小写字母,就把它换成对应的大写字母;其他字符(数字、标点)一律不动。

样例给的是 Luogu4!,里面有数字 4 和感叹号 !, 输出 LUOGU4! 说明这两者必须原样保留。 所以不能无脑对所有字符做加法——数字 '4' 加 32 会变成 'T',那就错了。

🎯 两层保险

· islower(c) 保证只对小写字母动手(数字、标点不会被误伤)
· toupper(c) 本身也保证只转小写字母,非字母原样返回

两个加起来就是「万无一失」。哪怕只写 s[i] = toupper(s[i]); 也是对的, 但加上 islower 判断,代码意图更清楚。

样例输入
Luogu4!
样例输出
LUOGU4!

完整代码

#include <iostream>
#include <string>
#include <cctype>                    // islower / toupper 都在这里
using namespace std;

int main(){
    string s;
    cin >> s;                        // 题目保证没有空格,用 cin 读最省事

    for(int i = 0; i < (int)s.size(); i++){
        if(islower(s[i]))            // ★ 是小写字母才转换
            s[i] = toupper(s[i]);
    }

    cout << s << endl;
    return 0;
}
💡 也可以一行搞定
for(char &c : s) c = toupper(c);   // 注意 &,否则改的是副本

范围 for 加上引用 &,就能直接在原字符串上改。写法短,但别忘了那个 &。

✅ 实测结果

官方样例 Luogu4! → 输出 LUOGU4! 完全一致
(数字 4 和感叹号 ! 都原样保留,说明没有误伤非字母字符)

自造数据 abcXYZ123 → 输出 ABCXYZ123 正确
(小写变大写、大写不动、数字不动)

易错点

  • 对所有字符无脑 - 32 → 数字和标点会被改坏。必须先判断是不是小写字母。
  • 用范围 for 但忘了 & → for(char c : s) c = toupper(c); 改的是副本, 输出还是原来的小写,看起来「代码没错但答案不对」。
  • 忘了 #include <cctype> → 编译报错说找不到 islower。
  • 这题没有空格,cin >> s 就够。但如果题目改成「可能含空格」, 就必须换成 getline,而且输出也不能用 cout << s 就完事(其实可以,只是读入要改)。
P1914 小书童——凯撒密码 入门 · 字符移位 + 取模

题意:密码是把原文字符串里每个字母往后移动 n 位得到的, 并且 z 的下一个字母是 a(绕回去)。 给原文和 n,求密码。

⚠️ 数据范围 / 关键约束

字符串长度 ≤ 50,1 ≤ n ≤ 26,字符串由小写字母组成。

两个细节值得注意:
· n 最大是 26,而小写字母正好 26 个。n = 26 时相当于原地不动, 正好能检验你的取模写对没有(26 % 26 = 0)。
· 只有小写字母,没有空格,所以可以用 cin >> 读。

思路

核心就是第 5 节那个「绕圈」公式。为什么不能直接 s[i] + n?

字符直接 +1 的结果问题
'q''r'正常
'z''{'❌ 跑到字母表外面去了,不是 'a'

所以必须先把字母换算成 0~25 的编号,加完偏移后对 26 取余, 再换算回字母。这样 'z'(编号 25)加 1 变成 26,26 % 26 = 0,回到 'a'。

顺带说一句:因为 'a' 的编号是 97,你可以直接拿字母本身来取模—— 但那样写出来的数字是 97、98 之类的,别人看不懂,自己过两天也看不懂。 减掉 'a' 让编号从 0 开始,代码才读得懂。

样例输入
1
qwe
样例输出
rxf

q→r、w→x、e→f,每个都往后一位。

完整代码

#include <iostream>
#include <string>
using namespace std;

int main(){
    int n;
    string s;
    cin >> n >> s;                   // n 是数字,s 是字母串(无空格)

    for(int i = 0; i < (int)s.size(); i++){
        // ★ 先转成 0~25,加 n 后对 26 取模,再转回字母
        s[i] = 'a' + (s[i] - 'a' + n) % 26;
    }

    cout << s << endl;
    return 0;
}
🎯 逐字拆解这一行

'a' + (s[i] - 'a' + n) % 26

· s[i] - 'a':把 'a'~'z' 变成 0~25
· + n:往后移 n 位,结果可能是 26~51
· % 26:把 26 及以上「折回来」,一定落回 0~25
· 'a' + ...:把编号变回字母

注意括号的位置:是先加 n 再取模,不是先取模再加 n。 如果写成 (s[i] - 'a') % 26 + n,那 z 就会变成编号 25 + 1 = 26, 再加 'a' 得到 '{',又跑出去了。

✅ 实测结果

官方样例 1 / qwe → 输出 rxf 完全一致

边界 26 / abcxyz → 输出 abcxyz 正确
(移 26 位等于没移,验证了 26 % 26 = 0 这个边界)

自造数据 13 / hello → 输出 uryyb 正确
(经典的 ROT13:h→u、e→r、l→y、l→y、o→b)

易错点

  • 直接 s[i] += n → z 之后的字母会跑到 { | } 这些符号上去,WA。
  • 取模位置写错:必须是 (c - 'a' + n) % 26,不能是 (c - 'a') % 26 + n。
  • 用 char 存中间结果时溢出:如果先 char t = s[i] + n;, n 大时可能超出 char 范围。直接在 int 里算完再赋回 s[i] 最安全(上面的写法就是这样)。
  • 读入顺序:n 在第一行、字符串在第二行,用 cin >> n >> s 一次读完最简洁。 如果非要混用 getline,别忘了 cin.ignore()。
  • 如果 n 的下限可能是 0 或者负数(本题不是),记得先 n = (n % 26 + 26) % 26; 规范化。
P1765 手机 普及- · 字符到按键次数映射

题意:老式手机键盘上,要按出字母得按数字键好几下 (比如按出 x 要按 9 键两下:第一下出 w,第二下变成 x)。 0 键按一下出一个空格。给一句只含小写字母和空格的句子, 问在手机上打出来至少要按多少下。

⚠️ 数据范围 / 关键约束

输入一行句子,只包含英文小写字母和空格,不超过 200 个字符。 (这道题的「说明/提示」只写了出处「NOI 导刊 2010 普及(10)」,没有额外数据范围,范围就在题目描述里。)

关键点:句子里有空格,所以必须用 getline 读。 而且空格本身也要计数(按 0 键一下)。

思路:先造一张「按键次数表」

题目给了键盘的排列方式(原题配了图)。老式手机的规律是:

数字键字母各要按几下
2a b c1 2 3
3d e f1 2 3
4g h i1 2 3
5j k l1 2 3
6m n o1 2 3
7p q r s1 2 3 4
8t u v1 2 3
9w x y z1 2 3 4
0(空格)1
🎯 怎么把这表变成代码

写一个长度 26 的数组 press[26], 用 c - 'a' 当下标('a'→0、'b'→1、……、'z'→25), 数组的值就是「按几下」。

注意 7 键和 9 键各有 4 个字母,所以这两个键最后一个是 4 下—— 这是本题最容易抄错的地方。

用样例验算一遍

输入是 i have a dream:

字符i空格have空格a空格dream合计
按键数3121321111321123

加起来正好 23,和样例输出一致。

样例输入
i have a dream
样例输出
23

完整代码

#include <iostream>
#include <string>
using namespace std;

// press[c - 'a'] = 按出字母 c 需要按几下
// 按键分组:2:abc  3:def  4:ghi  5:jkl  6:mno  7:pqrs  8:tuv  9:wxyz
int press[26] = {
    1, 2, 3,        // a b c
    1, 2, 3,        // d e f
    1, 2, 3,        // g h i
    1, 2, 3,        // j k l
    1, 2, 3,        // m n o
    1, 2, 3, 4,     // p q r s  ← 4 个字母,别漏了最后的 4
    1, 2, 3,        // t u v
    1, 2, 3, 4      // w x y z  ← 同上
};

int main(){
    string s;
    getline(cin, s);                 // ★ 句子里有空格,必须 getline

    int ans = 0;
    for(int i = 0; i < (int)s.size(); i++){
        if(s[i] == ' ') ans += 1;    // ★ 空格按 0 键,算一下
        else ans += press[s[i] - 'a'];
    }

    cout << ans << endl;
    return 0;
}
💡 数一下 press 里到底有没有 26 个数

写打表最容易出的错就是数错个数。用「每组换行 + 行末注释」的写法排好版, 一眼就能看出每组是不是 3 个(7 键、9 键那两组是 4 个)。
3×6 + 4×2 = 18 + 8 = 26,对上了。

✅ 实测结果

官方样例 i have a dream → 输出 23 完全一致

自造数据 s(单个字母,7 键按 4 下)→ 输出 4 正确
(专门用来验证 7 键 / 9 键那组的第 4 下有没有写对)

接近上限的长句:i have a dream 重复 13 遍(194 个字符)→ 输出 311 正确
(13 × 23 = 299,再加上 12 个分隔空格 × 1 = 12,共 311)

易错点

  • 用 cin >> s 读 → 只读到第一个空格为止,后面的内容全丢,答案会小很多。
  • 忘了给空格计数 → 样例会输出 19 而不是 23。
  • 打表抄错:7 键(pqrs)和 9 键(wxyz)是 4 个字母, 写成 1,2,3 就少一截,z 查表还会读到数组外面去。
  • 数组长度写 25 → 'z' - 'a' = 25 直接越界。必须有 26 个元素(下标 0~25)。
  • 句子里只有小写字母和空格,所以 else 分支不用再判别的字符类型。如果题目放宽到含大写字母, 就得先 tolower 再查表。

✗今日常见错误汇总

#错误写法后果正确写法
1cin >> n; getline(cin,s);s 读到空串,整行内容丢失中间加 cin.ignore();
2含空格的句子用 cin >> s只读到第一个空格getline(cin, s)
3for(char c : s) c = toupper(c);改的是副本,s 没变for(char &c : s)
4直接 c += n(凯撒密码)越过 'z' 变成 {'a' + (c-'a'+n) % 26
5对所有字符 -32 转大写数字、标点被改坏先 islower(c) 判断
6int press[25]'z' 越界(下标 25)int press[26]
7空格不计数P1765 答案偏小if(c==' ') ans += 1;
8忘了 #include <cctype>编译错误,找不到 islower加上头文件
9for(int i=0;i<s.size();i++)有符号/无符号比较警告(int)s.size()
10用 gets() / char 数组乱读gets 已从 C++ 中删除,编译不过统一用 std::string

✓自测:你真的会了吗

第 1 题:为什么 cin >> n; getline(cin, s); 会读到空串?

答:因为 cin >> n 只把数字读走,把这一行末尾的换行符留在了输入缓冲区里。
紧接着的 getline 一上来就撞到这个换行符,它认为「这一行已经结束」, 于是返回一个空串。真正想读的那一行还在缓冲区里没动。
解决:在两者之间加一句 cin.ignore();,把这一个残留字符吃掉。

第 2 题:'a' - 'A' 等于多少?有什么用?

答:等于 32。因为 'A' 是 65、'a' 是 97,97 − 65 = 32。
用途:大写变小写加 32,小写变大写减 32。
比如 'a' - 32 = 'A'。不过实际写代码时,用 toupper / tolower 更不容易错。

第 3 题:给一个字符 c,判断它是不是数字字符,有几种写法?

答:两种都行。

if(isdigit(c)) { ... }                     // 推荐,需 <cctype>
if(c >= '0' && c <= '9') { ... }           // 也可以,靠 ASCII 连续性

★ 注意:不能写成 c >= 0 && c <= 9—— 那是在比较字符和整数 0、9,而字符 '0' 的值是 48。 想手写区间就必须带单引号。

第 4 题:把 "Zz" 全部变成大写,结果是什么?

答:还是 "ZZ"……等等,原串是 "Zz",变大写之后是 "ZZ"。
过程:'Z' 本来就是大写,islower('Z') 是假,跳过; 'z' 是小写,toupper('z') → 'Z'。
要点:转换只对「该转换的字符」生效,已经是目标大小写的字符不会被重复处理出错。

第 5 题:凯撒密码,n = 26 的时候输出应该是什么?

输入是 abcxyz。

答:还是 abcxyz,原样输出。
因为字母只有 26 个,往后移 26 位正好转了一整圈回到自己。
用公式验算:'a' → (0 + 26) % 26 = 0 → 'a' ✔; 'z' → (25 + 26) % 26 = 51 % 26 = 25 → 'z' ✔。
这也是一个很好的自测数据——如果 n = 26 时答案变了,说明取模写错了。

第 6 题(手写代码):统计一个字符串里大写字母、小写字母、数字字符各有几个
int upper = 0, lower = 0, digit = 0;
for(int i = 0; i < (int)s.size(); i++){
    if(isupper(s[i])) upper++;
    else if(islower(s[i])) lower++;
    else if(isdigit(s[i])) digit++;
}
cout << upper << " " << lower << " " << digit << endl;

★ 想一想:为什么这里用 else if 而不是三个独立的 if?
答:三种类型互斥,用 else if 能少做判断,也更能表达「这三类分开统计」的意图。 用三个 if 结果也一样。

▤速查卡(截图存手机)

【必背 ASCII】
  '0' = 48    '9' = 57     // 数字连续
  'A' = 65    'Z' = 90     // 大写连续
  'a' = 97    'z' = 122    // 小写连续
  'a' - 'A' = 'a' - 'A' = 32

【读入(最容易错的地方)】
  cin >> s;                  // 遇空格停,适合单词/无空格串
  getline(cin, s);           // 读一整行,含空格
  cin >> n; cin.ignore();    // ★ 混用前必须加,否则 getline 读空串
  while(getline(cin, s)) {}  // 读所有行直到 EOF

【遍历】
  for(int i = 0; i < (int)s.size(); i++) ...   // 最稳
  for(char c : s) ...                          // 只读
  for(char &c : s) ...                          // ★ 要改就用这个

【字符函数(需 <cctype>)】
  isdigit / isalpha / isupper / islower / isalnum / isspace
  toupper(c)   tolower(c)

【大小写转换】
  if(islower(c)) c = toupper(c);
  c = c - 'a' + 'A';                       // 等价写法
  for(char &c : s) c = toupper(c);         // 整串转大写

【环形移位(凯撒密码)】
  s[i] = 'a' + (s[i] - 'a' + n) % 26;
  // 通用套路:转成 0 起 → 加偏移 → % 长度 → 加回起点

【字符→整数映射(打表)】
  int press[26] = { ... };      // 用 c - 'a' 当下标,长度必须 26
  int v = press[c - 'a'];
  // 7 键 pqrs、9 键 wxyz 各有 4 个字母,别抄错

【铁律】
  要空格 → getline;不要空格 → cin >>
  混用 → 中间加 cin.ignore()
  打表数组长度先数一遍

→明天预告 · Day 09

字符串进阶:数字、进制与格式处理

今天你学会了「把字符串当一串字符来处理」。明天要处理更麻烦的一类问题—— 字符串和数字之间来回转换,以及那些「格式特别讲究」的题。核心知识点大概有:

今天那 4 道题如果你都一遍过了,说明字符串的读入和遍历已经稳了——明天会直接往「格式细节」上强度,先把今天的地基踩实。