NOI C++ 课程体系 / 语法入门 / m3-02 第02讲
CSP-J 入门级 · 语法入门 · string类专题

C++ string 类 与相关函数

告别繁琐的字符数组,掌握现代 C++ 字符串处理的利器 —— 从声明初始化到常用函数,从实战案例到综合应用,一站式掌握 string 类。

6 大教学模块 5 个精讲案例 10 个常用函数 5 大易错点 3 道综合大题 15 道分层练习
本讲 · 第 16/56 讲string 类与相关函数
模块 01

情景导航

先用故事和生活中的场景建立直觉,再给出严谨的编程定义,最后用表格和口诀把核心要点固化下来。

故事故事引入 —— 小强的"密码本"困境

小强是一名编程初学者,老师布置了一个任务:写一个程序,让用户输入10个密码,然后检查这些密码是否都是有效的。

小强想了想,决定用 char 数组来存储密码。于是他定义了10个数组:

char pwd1[20], pwd2[20], pwd3[20], ..., pwd10[20];

代码写得又长又乱,每次操作一个密码都要记住是哪个数组名。更糟糕的是,有一次他想把两个密码拼在一起,结果数组越界,程序直接崩溃了!

老师看到后说:"你太辛苦了!用 string 类 吧,它会自动管理内存,拼接、查找、删除都方便得多。"

小强半信半疑地试了试,结果代码从 50 行缩减到了 10 行,而且再也没出过错!

①生活类比 —— string 类就像"智能记事本"

在写代码之前,先想一想生活里那些"存储文字信息"的场景。string 类就是对这些场景的编程抽象。

📦

快递单号

每个快递都有一个单号,长度不固定,有的短有的长。string 类就像快递单号,自动管理长度,不用担心装不下。

对应 string 自动扩容
🔑

密码

你的密码可能包含字母、数字、符号,长度任意。string 类可以存储任意字符,包括空格和特殊符号。

对应 string 存储任意字符
📔

日记本

你在日记本上写日记,想删掉某段就删,想复制一段就复制。string 类自带删除、复制、查找等功能。

对应 string 的 erase/copy/find
🏷️

商品标签

商品标签上有名称、价格、产地等信息,可以拼接在一起。string 类支持拼接操作,就像把标签信息组合起来。

对应 string 的 + 和 append
②编程定义 —— string 类是什么?

在 C++ 中,string 是一个标准库类(定义在 <string> 头文件中),用来表示文本字符串。它比传统的 char 数组更安全、更方便。

string 类的核心优势:"自动管理内存、支持直接运算、自带丰富操作函数"。

string vs char 数组:为什么要用 string?

特性 char 数组 string 类
声明 char s[20];(必须指定大小) string s;(自动分配)
赋值 strcpy(s, "hello"); s = "hello";(直接用 =)
拼接 strcat(s1, s2); s1 + s2 或 s1 += s2;
查找 strstr() 或手动遍历 s.find("abc")
长度 strlen(s) s.size() 或 s.length()
安全性 容易越界 自动管理,不易越界
③核心要点 —— string 类使用口诀

不管用 string 做什么操作,都离不开三个步骤:包含头文件 → 声明变量 → 调用函数。缺一不可。

记忆口诀

string 类不用愁,<string> 头文件先包含;
声明变量不指定,直接赋值最方便;
拼接用 + 号连,查找 find 来帮忙;
长度 size 来查询,删除 erase 最轻松;
替换 replace 搞定,截取 substr 最聪明;
三个要点记心头,string 操作不用愁!

🔗 衔接上一节

刚才我们讲了 string 类的定义和优势。那 string 类到底怎么用呢?它的语法格式是什么样的?接下来,我们就拆开 string 的"身体",看看它到底长什么样,以及有哪些常用的函数。

模块 02

知识探究

深入 string 类的语法细节,通过 5 个由浅入深的案例,掌握 string 类的各种操作。

1语法详解 —— string 的声明与初始化

string 类的声明与初始化非常简单,有 4 种常用方式:

string s1;
string s2 = "Hello";
string s3 (5, 'A');
string s4 (s2);
① 默认构造(空字符串) ② 直接赋值 ③ n个相同字符 ④ 拷贝构造
执行流程

Step 1:声明 string 变量(不需要指定大小)
Step 2:可以立即赋值,也可以稍后赋值
Step 3:使用 + 号拼接、find 查找、size 获取长度等操作
Step 4:string 对象会自动管理内存,无需手动释放

🎬 动画演示:string 变量声明与赋值过程

1
string s; —— 声明一个空字符串变量 s,内存中分配了空间但内容为空
2
s = "Hello"; —— 给 s 赋值为 "Hello",string 自动分配 5 个字符的空间
3
s += " World"; —— 拼接 " World",string 自动扩容到 11 个字符
4
此时 s 的内容为:"Hello World",长度为 11
5
string s2(s); —— 拷贝构造,s2 也变成了 "Hello World"
✓
string 类自动管理内存,无需担心越界和释放!

相关规则:

  • 使用 string 类必须包含 #include <string> 头文件
  • string 变量不需要指定大小,会自动根据内容调整
  • string 可以直接用 = 赋值,也可以用 + 拼接
  • string 支持直接用 ==、!=、<、> 比较大小(按字典序)
2易混淆概念对比辨析
char 数组操作
  • 必须指定大小:char s[20];
  • 赋值用 strcpy(s, "hello");
  • 拼接用 strcat(s1, s2);
  • 比较用 strcmp(s1, s2) == 0
  • 长度用 strlen(s)
  • 容易越界,需要手动管理内存
string 类操作
  • 不用指定大小:string s;
  • 直接赋值:s = "hello";
  • 拼接用 s1 + s2 或 s1 += s2;
  • 比较用 s1 == s2
  • 长度用 s.size() 或 s.length()
  • 自动管理内存,不易越界
3常用函数速查表 —— 10 个必备函数
函数用法说明示例
size() / length()s.size()返回字符串长度"hello".size() → 5
append() / +=s.append(t) 或 s += t在末尾追加字符串"Hi" + "!" → "Hi!"
find()s.find(str)查找子串首次出现的位置"hello".find("ll") → 2
substr()s.substr(pos, len)从 pos 开始截取 len 个字符"hello".substr(1,3) → "ell"
replace()s.replace(pos, len, str)从 pos 开始替换 len 个字符"hello".replace(1,2,"i") → "hilo"
erase()s.erase(pos, len)从 pos 开始删除 len 个字符"hello".erase(2,1) → "helo"
insert()s.insert(pos, str)在 pos 位置插入字符串"hi".insert(1,"ello") → "hello"
c_str()s.c_str()转为 C 风格字符串(char*)用于传给需要 char* 的函数
empty()s.empty()判断字符串是否为空"".empty() → true
at()s.at(i)获取第 i 个字符(带越界检查)"hello".at(0) → 'h'
4案例解析 —— 由浅入深 5 个案例

案例 1:字符串拼接与长度统计

题目:输入两个字符串,将它们拼接在一起,并输出拼接后的字符串和长度。

C++字符串拼接
#include <iostream>
#include <string>
using namespace std;
int main(){
    string s1, s2;
    cin >> s1 >> s2;
    string s3 = s1 + s2;  // 拼接两个字符串
    cout << "拼接结果: " << s3 << endl;
    cout << "长度: " << s3.size() << endl;
    return 0;
}

逐行解析:

代码行说明
string s1, s2;声明两个 string 变量,不需要指定大小
cin >> s1 >> s2;读取两个字符串(遇到空格会停止)
string s3 = s1 + s2;用 + 号拼接两个字符串,结果存入 s3
s3.size()获取拼接后字符串的长度
运行结果
输入: Hello World
拼接结果: Hello World
长度: 11

案例 2:查找与统计字符出现次数

题目:输入一个字符串和一个目标字符,统计该字符在字符串中出现的次数。(洛谷 P1308)

C++字符统计
#include <iostream>
#include <string>
using namespace std;
int main(){
    string s;
    char target;
    getline(cin, s);  // 读取整行(含空格)
    cin >> target;
    int cnt = 0;
    for(int i = 0; i < s.size(); i++){
        if(s[i] == target){
            cnt++;
        }
    }
    cout << cnt << endl;
    return 0;
}

逐行解析:

代码行说明
getline(cin, s);读取整行输入(包括空格),比 cin >> s 更全面
s[i] == target用下标访问第 i 个字符,与目标字符比较
cnt++匹配成功,计数器加 1
运行结果
输入: hello world
o
输出: 2

案例 3:替换与删除子串

题目:输入一个字符串,将其中的 "bad" 替换为 "good",并删除所有元音字母。(洛谷 P2010)

C++替换与删除
#include <iostream>
#include <string>
using namespace std;
int main(){
    string s;
    getline(cin, s);
    // 替换:将所有 "bad" 替换为 "good"
    size_t pos = s.find("bad");
    while(pos != string::npos){
        s.replace(pos, 3, "good");
        pos = s.find("bad", pos + 4);  // 从替换位置之后继续查找
    }
    // 删除:删除所有元音字母
    string vowels = "aeiouAEIOU";
    for(size_t i = 0; i < s.size(); ){
        if(vowels.find(s[i]) != string::npos){
            s.erase(i, 1);  // 删除第 i 个字符
            // 注意:删除后不增加 i,因为后面的字符前移了
        } else {
            i++;
        }
    }
    cout << s << endl;
    return 0;
}

逐行解析:

代码行说明
s.find("bad")查找 "bad" 首次出现的位置,找不到返回 string::npos
s.replace(pos, 3, "good")从 pos 开始,替换 3 个字符为 "good"
s.erase(i, 1)从位置 i 开始删除 1 个字符
注意:删除后 i 不增加因为删除后后面的字符前移,需要重新检查当前位置
运行结果
输入: this is a bad test
输出: ths s gd tst

案例 4:截取与分割字符串

题目:输入一个邮箱地址,提取用户名和域名部分。(洛谷 P1553)

C++截取邮箱信息
#include <iostream>
#include <string>
using namespace std;
int main(){
    string email;
    cin >> email;
    // 找到 @ 的位置
    size_t at_pos = email.find('@');
    if(at_pos != string::npos){
        string username = email.substr(0, at_pos);  // @ 之前的部分是用户名
        string domain = email.substr(at_pos + 1);    // @ 之后的部分是域名
        cout << "用户名: " << username << endl;
        cout << "域名: " << domain << endl;
    }
    return 0;
}

逐行解析:

代码行说明
email.find('@')找到 '@' 符号的位置
substr(0, at_pos)从位置 0 开始,截取 at_pos 个字符 → 用户名
substr(at_pos + 1)从 '@' 后面开始截取到末尾 → 域名
运行结果
输入: wangxiaoming@163.com
用户名: wangxiaoming
域名: 163.com

案例 5:密码强度检测器(综合应用)

题目:输入一个密码字符串,检测其强度等级:弱/中等/强。(洛谷 P1706)

C++密码强度检测
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
int main(){
    string pwd;
    cin >> pwd;
    int score = 0;
    bool hasUpper = false, hasLower = false;
    bool hasDigit = false, hasSpecial = false;
    for(char c : pwd){  // 范围 for 循环遍历每个字符
        if(isupper(c)) hasUpper = true;
        if(islower(c)) hasLower = true;
        if(isdigit(c)) hasDigit = true;
        if(!isalnum(c)) hasSpecial = true;
    }
    if(hasUpper) score++;
    if(hasLower) score++;
    if(hasDigit) score++;
    if(hasSpecial) score++;
    if(pwd.size() < 6) score--;  // 长度不足扣分
    if(score <= 1) cout << "弱" << endl;
    else if(score <= 3) cout << "中等" << endl;
    else cout << "强" << endl;
    return 0;
}

逐行解析:

代码行说明
for(char c : pwd)范围 for 循环,逐个访问 pwd 中的每个字符,简洁优雅
isupper(c)判断是否为大写字母(需 #include <cctype>)
isdigit(c)判断是否为数字
!isalnum(c)判断是否为特殊字符(非字母数字)
score 计分制每满足一个条件加 1 分,长度不足扣 1 分
运行结果
输入: abc123
输出: 中等

输入: Abc@123
输出: 强

🔗 衔接上一节

现在你已经掌握了 string 类的基本语法和常用函数。但光看不练假把式——接下来我们来做几道完整的编程题,把 string 类真正用起来!每道题都会给出题目描述、输入输出样例,还会提供多种解法供你比较。

模块 03

实践应用

通过完整的编程题目,练习 string 类的实际应用。每道题包含题目描述、输入输出样例、多种解法分析和完整代码。

题1回文判断

题目描述:输入一个字符串,判断它是否是回文。回文是指正读和反读都一样的字符串,如 "level"、"racecar"。

输入样例1:level

输出样例1:YES

输入样例2:hello

输出样例2:NO

方法一:双指针法

C++方法一:双指针
#include <iostream>
#include <string>
using namespace std;
int main(){
    string s;
    cin >> s;
    int left = 0, right = s.size() - 1;
    bool isPalin = true;
    while(left < right){
        if(s[left] != s[right]){
            isPalin = false;
            break;
        }
        left++;
        right--;
    }
    if(isPalin) cout << "YES" << endl;
    else cout << "NO" << endl;
    return 0;
}

方法二:反转比较法

C++方法二:反转比较
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int main(){
    string s, t;
    cin >> s;
    t = s;
    reverse(t.begin(), t.end());  // 反转字符串
    if(s == t) cout << "YES" << endl;
    else cout << "NO" << endl;
    return 0;
}

说明:方法一用双指针从两端向中间比较,效率更高(O(n/2));方法二利用 reverse 函数反转后直接比较,代码更简洁。

💡 引入思考

如果字符串中包含空格(如 "race car"),上面的代码还能正确判断吗?如果不能,该怎么修改?提示:可以先去掉空格再判断。

题2凯撒密码加密

题目描述:凯撒密码是一种简单的替换加密方法。将每个字母向后移动 k 个位置(如 k=3 时,a→d, b→e, ..., z→c)。输入一个字符串和一个偏移量 k,输出加密后的字符串。(洛谷 P1308)

输入样例:hello 3

输出样例:khoor

C++凯撒密码加密
#include <iostream>
#include <string>
using namespace std;
int main(){
    string s;
    int k;
    cin >> s >> k;
    for(int i = 0; i < s.size(); i++){
        if(isalpha(s[i])){  // 只加密字母
            char base = isupper(s[i]) ? 'A' : 'a';
            s[i] = (char)((s[i] - base + k) % 26 + base);
        }
    }
    cout << s << endl;
    return 0;
}

题目分析:

  • 用 for 循环遍历字符串的每个字符
  • isalpha 判断是否为字母,非字母不加密
  • 根据大写或小写确定 base('A' 或 'a')
  • 公式 (s[i] - base + k) % 26 实现循环移位
  • 最后加回 base 得到结果字符
运行结果
输入: hello 3
输出: khoor

输入: abc XYZ 1
输出: bcd YZA
题3单词统计与排序

题目描述:输入一行文本,统计其中有多少个单词,并将所有单词按字典序排序输出。(洛谷 P1308)

输入样例:hello world welcome to c++

输出样例:4 c++ hello to welcome world

C++单词统计与排序
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
int main(){
    string line, word;
    vector<string> words;
    getline(cin, line);
    // 按空格分割单词
    for(int i = 0; i < line.size(); i++){
        if(line[i] != ' '){
            word += line[i];
        } else {
            if(!word.empty()){
                words.push_back(word);
                word = "";
            }
        }
    }
    if(!word.empty()) words.push_back(word);
    sort(words.begin(), words.end());  // 按字典序排序
    cout << words.size() << endl;
    for(const string& w : words){
        cout << w << " ";
    }
    cout << endl;
    return 0;
}

题目分析:

  • 用 getline 读取整行(含空格)
  • 遍历字符,遇到非空格时拼接到 word,遇到空格时将 word 加入 vector
  • 用 sort 函数对字符串数组按字典序排序
  • 最后输出单词数量和排序后的结果
运行结果
输入: hello world welcome to c++
输出: 5
c++ hello to welcome world

🔗 衔接上一节

做完题目,是不是感觉 string 类也没那么可怕了?别急,接下来我们做个总结——把今天学到的核心要点梳理一遍,再给你一套"万能模板",以后遇到类似的题直接套用。最后还有拓展案例,帮你打开思路。

模块 04

总结提升

回顾 string 类的核心要点,掌握万能模板,并通过拓展案例深化理解。

1核心要点总结
string 类万能模板

#include <string> // 必须包含
string s; // 声明(不指定大小)
s = "hello"; // 赋值
s += "world"; // 拼接
s.size(); // 长度
s.find("abc"); // 查找
s.substr(pos, len); // 截取
s.replace(pos, len, str); // 替换
s.erase(pos, len); // 删除

知识点必须记住的内容
包含头文件#include <string> 是必须的,否则编译报错
声明变量string s; 不需要指定大小,自动管理内存
赋值s = "hello"; 直接用 = 赋值,比 strcpy 方便得多
拼接s1 + s2 或 s1 += s2,比 strcat 简洁
比较s1 == s2,比 strcmp(s1,s2)==0 直观
查找s.find("abc") 返回位置,找不到返回 string::npos
截取s.substr(pos, len) 从 pos 开始截取 len 个字符
删除s.erase(pos, len) 从 pos 开始删除 len 个字符
替换s.replace(pos, len, str) 从 pos 开始替换为 str
2拓展案例 —— 一题多解

拓展 1:字符串逆序(三种方法)

方法一:reverse 函数
C++
#include <string>
#include <algorithm>
string s = "hello";
reverse(s.begin(), s.end());
// s = "olleh"
方法二:双指针交换
C++
string s = "hello";
int l = 0, r = s.size() - 1;
while(l < r){
    swap(s[l], s[r]);
    l++; r--;
}
// s = "olleh"
方法三:从后往前拼接
C++
string s = "hello";
string t = "";
for(int i = s.size()-1; i >= 0; i--){
    t += s[i];
}
// t = "olleh"

拓展 2:统计元音和辅音字母(两种方法)

方法一:手动判断
C++
string s = "hello world";
int vowel = 0, consonant = 0;
for(char c : s){
    if(isalpha(c)){
        string v = "aeiouAEIOU";
        if(v.find(c) != string::npos) vowel++;
        else consonant++;
    }
}
方法二:用 count_if
C++
#include <algorithm>
int vowel = count_if(s.begin(), s.end(),
    [](char c){
        return string("aeiouAEIOU").find(c) != string::npos;
    });

使用 STL 的 count_if 和 Lambda 表达式,一行搞定

拓展 3:判断两个字符串是否是 Anagram(字母异位词)

方法一:排序比较
C++
string a = "listen", b = "silent";
sort(a.begin(), a.end());
sort(b.begin(), b.end());
if(a == b) cout << "是Anagram" << endl;
方法二:字符计数
C++
int cnt[26] = {0};
for(char c : a) cnt[c-'a']++;
for(char c : b) cnt[c-'a']--;
bool ok = true;
for(int i = 0; i < 26; i++)
    if(cnt[i] != 0) ok = false;

方法二时间复杂度 O(n),比方法一 O(n log n) 更快

3思考与启发

💡 对比上面的解法,你有什么启发?

1. 同一个问题可以有多种解法,有的简单直观(排序比较),有的高效优雅(字符计数)。

2. string 类提供了丰富的内置函数,善用这些函数可以让代码更简洁、更易读。

3. 当需要处理字符串时,优先考虑 string 类而不是 char 数组,安全性和便利性都更好。

4. 能否设计一个新算法解决本题?比如判断 Anagram 时,用哈希表代替排序,可以将时间复杂度从 O(n log n) 优化到 O(n)。

🔗 衔接上一节

总结完了,接下来该检验一下自己学得怎么样了。下面这5个错误,是初学者在使用 string 类时最容易踩的坑。看看你能不能全部找出来?每个错误都附有正确写法和预防口诀,帮你牢牢记住。

模块 05

学习检测

通过 5 个典型错误案例,学会识别和修复 string 类中的常见 bug。

错误 1

忘记包含 <string> 头文件

错误代码
#include <iostream>
using namespace std;
int main(){
    string s = "hello";  // 编译报错!
    cout << s << endl;
    return 0;
}
报错原因
没有包含 <string> 头文件,编译器不知道 string 是什么类型。
正确写法
#include <iostream>
#include <string>  // 必须包含!
using namespace std;
int main(){
    string s = "hello";
    cout << s << endl;
    return 0;
}
预防口诀:用 string 先包含,<string> 不能忘。
错误 2

find 返回值误判

错误代码
string s = "hello";
if(s.find("h")){  // 错!
    cout << "找到了" << endl;
}
报错原因
find 返回的是位置索引,"h" 在位置 0 处,s.find("h") 返回 0,而 0 在 if 条件中等价于 false!应该与 string::npos 比较。
正确写法
string s = "hello";
if(s.find("h") != string::npos){  // 正确!
    cout << "找到了" << endl;
}
预防口诀:find 返回位置值,不等于 npos 才算找到。
错误 3

erase 后索引未回退

错误代码
string s = "hello";
for(int i = 0; i < s.size(); i++){
    if(s[i] == 'l'){
        s.erase(i, 1);  // 错!
    }
}
报错原因
删除字符后,后面的字符会前移,但 i 仍然递增,导致跳过某些字符。比如删除第一个 'l' 后,第二个 'l' 移到了位置 2,但 i 变成了 3,跳过了它。
正确写法
string s = "hello";
for(int i = 0; i < s.size(); ){
    if(s[i] == 'l'){
        s.erase(i, 1);  // 删除后 i 不增加
    } else {
        i++;  // 没删除才递增
    }
}
预防口诀:删除字符后索引别递增,当前位置还要再检查。
错误 4

cin 无法读取空格

错误代码
string s;
cin >> s;  // 只能读到空格前
cout << s << endl;  // 输入 "hello world" 只输出 "hello"
报错原因
cin >> s 遇到空格就会停止读取,所以 "hello world" 只能读到 "hello"。要读取整行(含空格),应该用 getline。
正确写法
string s;
getline(cin, s);  // 读取整行(含空格)
cout << s << endl;  // 输入 "hello world" 输出 "hello world"
预防口诀:cin 读不到空格,getline 来帮忙。
错误 5

substr 越界

错误代码
string s = "hi";
string t = s.substr(0, 10);  // 可能出问题!
报错原因
substr 的第二个参数是截取长度,如果请求的长度超过字符串剩余长度,C++ 会截取到末尾(不会报错)。但如果是 at() 方法越界,会抛出 out_of_range 异常。
正确写法
string s = "hi";
int len = min(10, (int)s.size());
string t = s.substr(0, len);  // 安全截取
// 或者用 at() 时加 try-catch
预防口诀:substr 越界不报错,手动控制长度更安全。
速查表string 类易错点速查
错误类型症状原因解决方法
未包含头文件编译报错忘记 #include <string>始终包含 <string>
find 返回值误判位置0被误判为未找到if(s.find("x")) 中0为false与 string::npos 比较
erase 后索引未回退跳过字符删除后 i 仍递增删除后 i 不增加
cin 读空格只读到空格前cin >> s 遇空格停止用 getline(cin, s)
substr 越界截取异常请求长度超过字符串用 min 控制长度
at() 越界抛出异常s.at(i) 中 i 超出范围先检查 s.size()

🔗 衔接上一节

检测完了,错误都改过来了吧?最后来几道练习题巩固一下。作业分三层:A基础、B提高、C挑战,难度递增。每道题都有答案和解析,点击即可展开。做完记得对照答案,看看自己哪里还需要加强。

📌知识点笔记 —— string 类速记卡

学完 string 类,把下面这些关键知识点记在小本本上,随时复习。

📝 语法模板

#include <string>

string s = "hello";

s += " world";

🔍 常用函数

① s.size() / s.length() — 长度

② s.find("abc") — 查找

③ s.substr(pos, len) — 截取

④ s.replace(pos, len, str) — 替换

⑤ s.erase(pos, len) — 删除

⚠️ 常见陷阱

• 忘记包含 <string>

• find 返回值与 npos 比较

• erase 后索引不回退

• cin 读不到空格,用 getline

🔧 常用技巧

• 拼接:s1 + s2 或 s1 += s2

• 比较:s1 == s2(字典序)

• 遍历:for(char c : s)

• 转 C 字符串:s.c_str()

🔄 string vs char[]

• string 自动管理内存,char[] 容易越界

• string 支持 +、== 等运算符

• string 有 find、substr 等丰富函数

• 优先使用 string,除非需要底层控制

💡 记忆口诀

string 类不用愁,<string> 头文件先包含;

声明变量不指定,直接赋值最方便;

拼接用 + 号连,查找 find 来帮忙;

长度 size 来查询,删除 erase 最轻松;

替换 replace 搞定,截取 substr 最聪明;

三个要点记心头,string 操作不用愁!

课后练习

练习作业

巩固 string 类,分层练习,点击展开答案与解析。

总结练习3 道综合题
A 基础 洛谷 P1308 统计单词数

题目:统计一行文本中某个单词出现的次数

题目描述:给定一行文本和一个单词,统计该单词在文本中出现的次数(不区分大小写)。

输入样例:Hello hello World hello hello

输出样例:3

提示:先将文本和搜索词统一转为小写,再用 find 逐个查找。
点击查看参考答案与解析
C++参考代码
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
string toLower(string s){
    for(char &c : s) c = tolower(c);
    return s;
}
int main(){
    string text, word;
    getline(cin, text);
    cin >> word;
    text = toLower(text);
    word = toLower(word);
    int cnt = 0, pos = 0;
    while((pos = text.find(word, pos)) != string::npos){
        cnt++;
        pos += word.size();
    }
    cout << cnt << endl;
    return 0;
}

解析:先用 toLower 统一转小写,再用 while+find 循环查找所有出现位置,每次找到后从下一个位置继续搜索。

B 提高 洛谷 P1706 字符串加密

题目:字符串加密——将每个字母替换为下一个字母

题目描述:输入一个字符串,将其中每个字母替换为字母表中的下一个字母(a→b, z→a, A→B, Z→A),非字母字符保持不变。

输入样例:xyz XYZ

输出样例:yza YZA

提示:遍历字符串,判断字母类型后做移位,z→a 和 Z→A 需要特殊处理。
点击查看参考答案与解析
C++参考代码
#include <iostream>
#include <string>
using namespace std;
int main(){
    string s;
    getline(cin, s);
    for(char &c : s){
        if(isalpha(c)){
            if(c == 'z') c = 'a';
            else if(c == 'Z') c = 'A';
            else c++;
        }
    }
    cout << s << endl;
    return 0;
}

解析:遍历每个字符,如果是普通字母则 c++,如果是 z 或 Z 则特殊处理为 a 或 A。

C 挑战 洛谷 P1553 数字变换

题目:提取字符串中的所有数字并求和

题目描述:输入一个包含字母、数字和空格的字符串,提取其中所有连续数字组成的整数,并求它们的和。

输入样例:abc123 def 45 ghi6

输出样例:174(123 + 45 + 6 = 174)

提示:遍历字符串,遇到数字时累积构建整数,遇到非数字时将累积的数加入总和。
点击查看参考答案与解析
C++参考代码
#include <iostream>
#include <string>
using namespace std;
int main(){
    string s;
    getline(cin, s);
    long long sum = 0, cur = 0;
    for(char c : s){
        if(isdigit(c)){
            cur = cur * 10 + (c - '0');
        } else {
            if(cur != 0){
                sum += cur;
                cur = 0;
            }
        }
    }
    if(cur != 0) sum += cur;  // 处理末尾数字
    cout << sum << endl;
    return 0;
}

解析:遍历字符串,遇到数字字符时累积构建整数(cur = cur*10 + (c-'0')),遇到非数字时将 cur 加到 sum 并重置 cur。注意处理末尾数字的情况。

分层作业12 道作业题(A基础 / B提高 / C挑战)

A 组 · 基础(4 题)

A1 基础字符串反转

题目:输入一个字符串,输出其反转后的字符串

输入:hello

输出:olleh

提示:用 reverse 函数或双指针交换。
查看参考答案
C++
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int main(){
    string s;
    cin >> s;
    reverse(s.begin(), s.end());
    cout << s << endl;
    return 0;
}

思路:用 algorithm 头文件里的 reverse(s.begin(), s.end()) 即可原地翻转 string。

易错:忘了 #include ,reverse 会编译报错。

A2 基础字符统计

题目:统计字符串中大写字母、小写字母和数字的个数

输入:Hello World 123

输出:2 8 3

提示:遍历字符串,用 isupper、islower、isdigit 分别统计。
查看参考答案
C++
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
int main(){
    string s;
    getline(cin, s);
    int upper = 0, lower = 0, digit = 0;
    for(char c : s){
        if(isupper(c)) upper++;
        else if(islower(c)) lower++;
        else if(isdigit(c)) digit++;
    }
    cout << upper << " " << lower << " " << digit << endl;
    return 0;
}

思路:范围 for 遍历每个字符,用 isupper / islower / isdigit 分类累加三个计数器。

易错:分类判断要用 if / else if 互斥,一个字符只计一类;含空格整行要用 getline 读入。

A3 基础字符串比较

题目:输入两个字符串,按字典序输出较小的那个

输入:banana apple

输出:apple

提示:string 支持直接用 < 比较字典序。
查看参考答案
C++
#include <iostream>
#include <string>
using namespace std;
int main(){
    string a, b;
    cin >> a >> b;
    if(a < b) cout << a << endl;
    else cout << b << endl;
    return 0;
}

思路:string 直接用 < 比较就是按字典序,输出较小的那个即可。

易错:别以为 < 比较的是长度——string 的比较是逐字符按字典序进行的。

A4 基础替换空格

题目:将字符串中的所有空格替换为 "*"

输入:hello world

输出:hello*world

提示:遍历字符串,遇到空格就替换。
查看参考答案
C++
#include <iostream>
#include <string>
using namespace std;
int main(){
    string s;
    getline(cin, s);
    for(char &c : s){
        if(c == ' ') c = '*';
    }
    cout << s << endl;
    return 0;
}

思路:范围 for 必须用引用 char& c 遍历,遇到空格直接改成 '*'。

易错:写成 char c(不带 &)修改的是副本,原字符串一个空格都不会变。

B 组 · 提高(4 题)

B1 提高洛谷 P1308统计单词数

题目:统计一行文本中某个单词出现的次数(不区分大小写)

输入:Hello hello World hello hello

输出:2

提示:统一转小写后用 find 循环查找。
查看参考答案
C++
#include <iostream>
#include <string>
using namespace std;
string toLower(string s){
    for(char &c : s) c = tolower(c);
    return s;
}
int main(){
    string text, word;
    getline(cin, text);
    cin >> word;
    text = toLower(text);
    word = toLower(word);
    int cnt = 0, pos = 0;
    while((pos = text.find(word, pos)) != string::npos){
        cnt++;
        pos += word.size();
    }
    cout << cnt << endl;
    return 0;
}

思路:把整行文本和目标单词都转成小写,再拆出每个完整单词逐一比对统计。

易错:忘了统一转小写会导致大小写不同算不上同一个词;也不能只用 find,它会匹配到子串而不是完整单词。

B2 提高回文判断

题目:判断一个字符串是否是回文

输入:racecar

输出:YES

提示:双指针从两端向中间比较,或用 reverse 反转后比较。
查看参考答案
C++
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int main(){
    string s, t;
    cin >> s;
    t = s;
    reverse(t.begin(), t.end());
    cout << (s == t ? "YES" : "NO") << endl;
    return 0;
}

思路:复制一份 t = s,反转 t 后与原串比较,相等就是回文。

易错:reverse 是原地修改,必须先复制再反转,直接反转会丢掉原串。

B3 提高字母异位词

题目:判断两个字符串是否是字母异位词(Anagram)

输入:listen silent

输出:YES

提示:排序后比较,或用字符计数数组。
查看参考答案
C++
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int main(){
    string a, b;
    cin >> a >> b;
    sort(a.begin(), a.end());
    sort(b.begin(), b.end());
    cout << (a == b ? "YES" : "NO") << endl;
    return 0;
}

思路:把两个串分别 sort 后比较,排序结果相同就是字母异位词。

易错:直接比较原串是错的——异位词字母相同但顺序不同,必须先排序。

B4 提高凯撒密码

题目:实现凯撒密码的加密和解密

输入:encrypt hello 3

输出:khoor

提示:加密时每个字母后移 k 位,解密时前移 k 位(注意 z→c 的循环)。
查看参考答案
C++
#include <iostream>
#include <string>
using namespace std;
int main(){
    string op, s;
    int k;
    cin >> op >> s >> k;
    for(char &c : s){
        if(isalpha(c)){
            char base = isupper(c) ? 'A' : 'a';
            int offset = (op == "decrypt") ? -k : k;
            c = (char)((c - base + offset % 26 + 26) % 26 + base);
        }
    }
    cout << s << endl;
    return 0;
}

思路:字母以 'A' 或 'a' 为基准平移 k 位,减 base 后 % 26 再加回 base,解密用减法或负位移。

易错:平移后没先减 base 就 % 26 会超出字母范围;非字母字符要原样保留不做平移。

C 组 · 挑战(4 题)

C1 挑战最长回文子串

题目:找出一个字符串中最长的回文子串

输入:abacdfgdcaba

输出:aba

提示:枚举所有子串,用 find 和 rfind 判断回文。
查看参考答案
C++
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
bool isPalin(string s){
    string t = s;
    reverse(t.begin(), t.end());
    return s == t;
}
int main(){
    string s, best = "";
    cin >> s;
    for(int i = 0; i < s.size(); i++)
        for(int j = i+1; j <= s.size(); j++){
            string sub = s.substr(i, j-i);
            if(isPalin(sub) && sub.size() > best.size())
                best = sub;
        }
    cout << best << endl;
    return 0;
}

思路:枚举每个起点和终点,用辅助函数反转判断是否回文,更新最长的答案。

易错:s.substr(i, len) 第二个参数是长度不是终点下标,写错会截错子串。

C2 挑战括号匹配

题目:判断括号字符串是否有效(洛谷 P1739)

输入:([{}])

输出:YES

提示:用栈的思想,遇到左括号入栈,遇到右括号检查栈顶是否匹配。
查看参考答案
C++
#include <iostream>
#include <string>
#include <stack>
using namespace std;
int main(){
    string s;
    getline(cin, s);
    stack<char> st;
    for(char c : s){
        if(c == '(' || c == '[' || c == '{') st.push(c);
        else {
            if(st.empty()){
                cout << "NO" << endl;
                return 0;
            }
            char top = st.top();
            if((c==')' && top!='(') || (c==']' && top!='[') || (c=='}' && top!='{')){
                cout << "NO" << endl;
                return 0;
            }
            st.pop();
        }
    }
    cout << (st.empty() ? "YES" : "NO") << endl;
    return 0;
}

思路:左括号入栈,右括号与栈顶配对弹出,结束时栈必须为空才有效。

易错:遇到右括号要先判 st.empty() 再 top(),空栈取 top 是未定义行为;三种括号要分别配对。

C3 挑战字符串压缩

题目:字符串压缩——连续相同字符用"字符+次数"表示

输入:aaabbc

输出:a3b2c1

提示:遍历字符串,统计连续相同字符的个数。
查看参考答案
C++
#include <iostream>
#include <string>
using namespace std;
int main(){
    string s, result = "";
    cin >> s;
    int cnt = 1;
    for(int i = 1; i <= s.size(); i++){
        if(i < s.size() && s[i] == s[i-1]){
            cnt++;
        } else {
            result += s[i-1] + to_string(cnt);
            cnt = 1;
        }
    }
    cout << result << endl;
    return 0;
}

思路:从 i=1 起,与前一个字符相同计数加一,不同就把“字符+次数”拼进结果,循环走到 i == s.size() 时收尾输出最后一组。

易错:循环结束后忘了输出最后一组(代码靠 i 到达 s.size() 触发),次数拼接前要转成字符串。

C4 挑战Z字形变换

题目:将字符串按 Z 字形排列后按行读取(洛谷 P6157)

输入:PAYPALISHIRING 3

输出:PAHNAPLSIIGYIR

提示:模拟 Z 字形填写过程,用 vector<string> 表示每一行。
查看参考答案
C++
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main(){
    string s;
    int n;
    cin >> s >> n;
    if(n <= 1){ cout << s << endl; return 0; }
    vector<string> rows(n);
    int cur = 0, dir = 1;
    for(char c : s){
        rows[cur] += c;
        if(cur == 0) dir = 1;
        if(cur == n-1) dir = -1;
        cur += dir;
    }
    string result;
    for(auto &r : rows) result += r;
    cout << result << endl;
    return 0;
}

思路:用 n 个 string 当 n 行,方向变量 step 控制当前行号,到达首尾行就折返。

易错:n=1 时直接输出原串,否则方向判断会出问题;行号越界也是常见错误。