从「数字的世界」走进「文字的世界」——顺便拆掉新手最常踩的那颗地雷:getline
| 0 - 20 min | 字符与 ASCII 的关系(第 1 节) |
| 20 - 50 min | cin >> 与 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 练的「按位处理」(把一串东西拆成一个个元素逐个处理)今天会以新形式出现——
那时拆的是数字的十进制位,今天拆的是字符串里的字符。
电脑里存不下「字母 A」这种东西,它只认识数字。所以每个字符都被编了一个号, 这个编号表就叫 ASCII 码表。
比如字母 'A' 的编号是 65,'a' 是 97。
你写 'A' 的时候,电脑看到的其实就是 65。
既然字符本质上就是整数,那么:
· 字符可以直接比较大小(比的是编号)
· 字符可以做加减法('a' + 1 就是 'b')
· 字符可以直接当数组下标(Day 6 学过的那种桶)
| 字符 | ASCII 码 | 怎么记 |
|---|---|---|
'0' | 48 | 数字字符从 48 开始 |
'9' | 57 | 48 + 9,所以 '9' - '0' = 9 |
'A' | 65 | 大写字母从 65 开始 |
'Z' | 90 | 65 + 25,一共 26 个 |
'a' | 97 | 小写字母从 97 开始 |
'z' | 122 | 97 + 25 |
' '(空格) | 32 | 比所有可见字符都小 |
① 三组字符各自连续。'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 是哪来的」的疑问,也少一次手滑。
cin >> 和 getline| 写法 | 遇到空格 / 制表符会怎样 | 遇到换行会怎样 | 适合读什么 |
|---|---|---|---|
cin >> s | 停住,只读走前面那一截 | 留在缓冲区里,不读走 | 不带空格的单词 / 号码 |
getline(cin, s) | 连空格一起读进去 | 读走并丢掉,作为结束标志 | 含空格的整行句子 |
cin >> 是「挑一个词」——它会跳过前面的空白,读到下一个空白就停,并且把那个空白留在原地。
getline 是「抄一整行」——它从当前位置一直抄到换行符为止,然后把换行符吃掉。
假设输入长这样:
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 >>,是最省心的策略。
坑 A:只读一行够不够?本题 P5015 只有一行。但如果题目说「若干行」,你要写成
while(getline(cin, s)) { ... } 一直读到文件结束(EOF)。
坑 B:Window 下的 \r。换行在不同系统里可能是 \n(Linux)
或 \r\n(Windows)。getline 只吃掉 \n,
如果输入文件是 Windows 格式,s 末尾就会多一个看不见的 '\r',
统计字符数时会多算 1。洛谷用的是 Linux 换行,本地测完记得心里有数。
(今天的 P5015 只统计「不是空格的字符」,就是用这个思路做的。)
字符串可以理解成「一堆字符排成的一排」,所以 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) 转换,这一类陷阱就绕开了。
<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) 完全没问题,
这里只是让你知道有这么回事,以后处理中文或二进制数据时再想起来。
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' | 16 | 17 | 17 | 'r' |
| 'w' | 22 | 23 | 23 | 'x' |
| 'e' | 4 | 5 | 5 | 'f' |
| 'z' | 25 | 26 | 0 | 'a' ✔ |
s[i] = 'a' + (s[i] - 'a' + n) % 26;
「先转成 0 起的编号 → 加偏移 → 对长度取余 → 再加回起点」这四步, 以后做环形数组、循环队列、环形棋盘都会用到,不只字母能用。
这里的 % 26 和昨天蛇形方阵的 (d + 1) % 4 是完全同一个套路。
下面每道题的代码我都用 g++ -std=c++14 -O2 -Wall 实际编译运行过,输出是你看到的真实结果。
题意:给一个作文标题,问标题里有多少个字符。 标题里可能有大小写字母、数字、空格和换行符,但统计时空格和换行符不算。
输入只有一行。|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 读出来的字符串里本来就不包含换行符,
它把换行符当结束标志吃掉了。所以我们只要排除空格就行。
234
3
Ca 45
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。cin >> c)→ 标题不止一个字符,直接错。while(getline(cin, s)) 累加,
在洛谷上其实也能过,但没必要,而且如果行末有多余空行就会出错。cin >> 读数字,记得先 cin.ignore()(这题没有,但要养成条件反射)。题意:办公软件会自动把字母变成大写。输入一个字符串, 把里面所有小写字母变成大写字母,其余字符原样输出。
题目描述里写明:输入字符串长度不超过 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(char c : s) c = toupper(c); 改的是副本,
输出还是原来的小写,看起来「代码没错但答案不对」。#include <cctype> → 编译报错说找不到 islower。cin >> s 就够。但如果题目改成「可能含空格」,
就必须换成 getline,而且输出也不能用 cout << s 就完事(其实可以,只是读入要改)。题意:密码是把原文字符串里每个字母往后移动 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] 最安全(上面的写法就是这样)。cin >> n >> s 一次读完最简洁。
如果非要混用 getline,别忘了 cin.ignore()。n = (n % 26 + 26) % 26; 规范化。题意:老式手机键盘上,要按出字母得按数字键好几下
(比如按出 x 要按 9 键两下:第一下出 w,第二下变成 x)。
0 键按一下出一个空格。给一句只含小写字母和空格的句子,
问在手机上打出来至少要按多少下。
输入一行句子,只包含英文小写字母和空格,不超过 200 个字符。 (这道题的「说明/提示」只写了出处「NOI 导刊 2010 普及(10)」,没有额外数据范围,范围就在题目描述里。)
关键点:句子里有空格,所以必须用 getline 读。
而且空格本身也要计数(按 0 键一下)。
题目给了键盘的排列方式(原题配了图)。老式手机的规律是:
| 数字键 | 字母 | 各要按几下 |
|---|---|---|
| 2 | a b c | 1 2 3 |
| 3 | d e f | 1 2 3 |
| 4 | g h i | 1 2 3 |
| 5 | j k l | 1 2 3 |
| 6 | m n o | 1 2 3 |
| 7 | p q r s | 1 2 3 4 |
| 8 | t u v | 1 2 3 |
| 9 | w x y z | 1 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 | 空格 | h | a | v | e | 空格 | a | 空格 | d | r | e | a | m | 合计 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 按键数 | 3 | 1 | 2 | 1 | 3 | 2 | 1 | 1 | 1 | 1 | 3 | 2 | 1 | 1 | 23 |
加起来正好 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 读 → 只读到第一个空格为止,后面的内容全丢,答案会小很多。1,2,3 就少一截,z 查表还会读到数组外面去。'z' - 'a' = 25 直接越界。必须有 26 个元素(下标 0~25)。else 分支不用再判别的字符类型。如果题目放宽到含大写字母,
就得先 tolower 再查表。| # | 错误写法 | 后果 | 正确写法 |
|---|---|---|---|
| 1 | cin >> n; getline(cin,s); | s 读到空串,整行内容丢失 | 中间加 cin.ignore(); |
| 2 | 含空格的句子用 cin >> s | 只读到第一个空格 | getline(cin, s) |
| 3 | for(char c : s) c = toupper(c); | 改的是副本,s 没变 | for(char &c : s) |
| 4 | 直接 c += n(凯撒密码) | 越过 'z' 变成 { | 'a' + (c-'a'+n) % 26 |
| 5 | 对所有字符 -32 转大写 | 数字、标点被改坏 | 先 islower(c) 判断 |
| 6 | int press[25] | 'z' 越界(下标 25) | int press[26] |
| 7 | 空格不计数 | P1765 答案偏小 | if(c==' ') ans += 1; |
| 8 | 忘了 #include <cctype> | 编译错误,找不到 islower | 加上头文件 |
| 9 | for(int i=0;i<s.size();i++) | 有符号/无符号比较警告 | (int)s.size() |
| 10 | 用 gets() / char 数组乱读 | gets 已从 C++ 中删除,编译不过 | 统一用 std::string |
cin >> n; getline(cin, s); 会读到空串?答:因为 cin >> n 只把数字读走,把这一行末尾的换行符留在了输入缓冲区里。
紧接着的 getline 一上来就撞到这个换行符,它认为「这一行已经结束」,
于是返回一个空串。真正想读的那一行还在缓冲区里没动。
解决:在两者之间加一句 cin.ignore();,把这一个残留字符吃掉。
'a' - 'A' 等于多少?有什么用?答:等于 32。因为 'A' 是 65、'a' 是 97,97 − 65 = 32。
用途:大写变小写加 32,小写变大写减 32。
比如 'a' - 32 = 'A'。不过实际写代码时,用 toupper / tolower 更不容易错。
c,判断它是不是数字字符,有几种写法?答:两种都行。
if(isdigit(c)) { ... } // 推荐,需 <cctype>
if(c >= '0' && c <= '9') { ... } // 也可以,靠 ASCII 连续性
★ 注意:不能写成 c >= 0 && c <= 9——
那是在比较字符和整数 0、9,而字符 '0' 的值是 48。
想手写区间就必须带单引号。
"Zz" 全部变成大写,结果是什么?答:还是 "ZZ"……等等,原串是 "Zz",变大写之后是 "ZZ"。
过程:'Z' 本来就是大写,islower('Z') 是假,跳过;
'z' 是小写,toupper('z') → 'Z'。
要点:转换只对「该转换的字符」生效,已经是目标大小写的字符不会被重复处理出错。
输入是 abcxyz。
答:还是 abcxyz,原样输出。
因为字母只有 26 个,往后移 26 位正好转了一整圈回到自己。
用公式验算:'a' → (0 + 26) % 26 = 0 → 'a' ✔;
'z' → (25 + 26) % 26 = 51 % 26 = 25 → 'z' ✔。
这也是一个很好的自测数据——如果 n = 26 时答案变了,说明取模写错了。
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() 打表数组长度先数一遍
字符串进阶:数字、进制与格式处理
今天你学会了「把字符串当一串字符来处理」。明天要处理更麻烦的一类问题—— 字符串和数字之间来回转换,以及那些「格式特别讲究」的题。核心知识点大概有:
to_string(x)、stoi(s) / stoll(s)% 10、/ 10,还要注意末尾的 0 和负号0.5000 变成 0.5)今天那 4 道题如果你都一遍过了,说明字符串的读入和遍历已经稳了——明天会直接往「格式细节」上强度,先把今天的地基踩实。