NOI C++ 课程体系 / 语法入门 / m3-01 第01讲
CSP-J 入门级 · 语法入门 · 字符数组专题

C++ 字符数组与相关函数 完全指南

让计算机处理文本信息 —— 从字符数组的定义到字符串函数,从密码加密到回文判断,一站式掌握字符数组。

6 大教学模块 5 个精讲案例 9 个常用函数 5 大易错点 5 道学习检测 15 道分层练习
本讲 · 第 15/56 讲字符数组与相关函数
模块 01

情景导航

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

故事故事引入 —— 小强的密码本

小强是一名编程初学者,他最近想写一个用户登录程序。他先用变量来存储用户名和密码:

C++用变量存储用户名和密码
char name1 = 'W';
char name2 = 'a';
char name3 = 'n';
char name4 = 'g';
char name5 = 'X';
char name6 = 'u';
char name7 = 'n';
char name8 = ' ';
char name9 = 'L';
char name10 = 'a';
char name11 = 'o';
char name12 = 'n';
char name13 = 'g';
char pwd1 = '1';
char pwd2 = '2';
char pwd3 = '3';
char pwd4 = '4';
// 天哪!用户名 "WangXiong" 需要13个变量!密码 "1234" 需要4个变量!

小强瘫在了椅子上:"这太疯狂了!如果用户名是100个字符呢?难道要写100个变量吗?"

这时,老师走过来,笑着说:"你需要的不是100个变量,而是一个字符数组。"

💡 小强的困境揭示了什么问题?

当我们需要存储一串字符(比如用户名、密码、句子)时,用单独的变量一个一个存,既繁琐又不方便操作。我们需要一种能一次性存储多个字符的数据结构。

方式用户名 "WangXiong"密码 "1234"
13个变量name1~name13pwd1~pwd4
1个字符数组char name[100]char pwd[100]

显然,字符数组是更优雅的选择。那么,什么是字符数组?它和普通数组有什么区别?

①生活类比 —— 字符数组就是"一排字母信箱"

在写代码之前,先想一想生活里那些"一排格子,每个格子放一个东西"的场景。字符数组正是对这些场景的模仿。

🔐

密码锁

密码锁有4个数字位,每个位放一个数字(0-9)。组合起来就是密码。字符数组就是"一排格子,每个格子放一个字符"。

对应 char arr[4] = {'1','2','3','4'}
📝

字母卡片

你有一排卡片,每张卡片写一个字母,排成一排就能拼出单词。字符数组就是"一排字母卡片"。

对应 char word[6] = {'H','e','l','l','o'}
📬

邮箱编号

小区有100个邮箱,每个邮箱有一个编号(0-99)。你要找第5个邮箱,直接去5号就行。字符数组也是"用下标找字符"。

对应 arr[5] 访问第6个字符
🎫

电影票座位

一排座位,每个座位坐一个人。座位号从0开始。第0个座位坐A,第1个坐B……字符数组的下标也是从0开始的。

对应 arr[0]='A', arr[1]='B'
②编程定义 —— 字符数组
char 数组名[数组长度];
char = 字符类型关键字 数组名 = 变量名 数组长度 = 存储字符个数

字符数组是专门用来存储字符的数组。它的每个元素都是 char 类型的字符。

C++字符数组的基本定义
char name[100];        // 定义一个能存100个字符的数组
char pwd[10];         // 能存10个字符
char msg[5] = {'H','e','l','l','o'};  // 定义并初始化

核心要点:

  • •字符数组的每个元素存储一个 char 类型的字符
  • •下标从 0 开始,最大下标是 长度-1
  • •字符串在字符数组末尾自动添加 '\0' 作为结束标志
  • •可以用 字符串直接初始化:char s[] = "Hello";
③核心要点与口诀
🔑 字符数组记忆口诀

字符数组 char 定义,方括号内写长度。
下标从0开始数,越界访问会出错。
字符串末尾有 \0, strlen 不算它。
初始化用引号双,自动加零不用愁。

模块 02

知识探究

深入理解字符数组的语法、内存布局、相关函数,通过案例由浅入深掌握字符数组的用法。

🔧语法详解 —— 字符数组的声明与初始化

字符数组有 4种初始化方式,每种方式都有其适用场景。

C++4种初始化方式
// 方式1:逐个字符初始化
char s1[5] = {'H', 'e', 'l', 'l', 'o'};

// 方式2:用字符串直接初始化(自动加\0)
char s2[6] = "Hello";

// 方式3:不指定长度(自动计算)
char s3[] = "Hello";  // 长度自动为6(含\0)

// 方式4:全部初始化为0(\0)
char s4[100] = {0};  // 全部元素为\0
对比4种初始化方式对照表
方式语法长度末尾\0适用场景
方式1{ch1,ch2,...}显式指定无(需手动加)精确控制每个字符
方式2"string"显式指定自动添加已知字符串内容
方式3"string"自动计算自动添加最常用,简洁
方式4{0}显式指定全部为\0清空数组/预留空间
🧠内存布局可视化 —— 字符数组在内存中长什么样

字符数组在内存中是连续存储的。用字符串初始化时,编译器会自动在末尾添加 '\0'(ASCII码为0)。

char word[6] = "Hello";
0 内存位置: 'H' | 'e' | 'l' | 'l' | 'o' | '\0'
1 下标: 0 | 1 | 2 | 3 | 4 | 5
2 word[0]='H', word[1]='e', word[2]='l', word[3]='l', word[4]='o', word[5]='\0'
3 strlen(word) = 5(不计算\0),sizeof(word) = 6(计算\0)
C++验证内存布局
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char word[6] = "Hello";
    for (int i = 0; i < 6; i++) {
        cout << "word[" << i << "] = " << (int)word[i] << " ('" << word[i] << "')" << endl;
    }
    // word[0] = 72 ('H')  word[1] = 101 ('e')
    // word[2] = 108 ('l')  word[3] = 108 ('l')
    // word[4] = 111 ('o')  word[5] = 0 ('\0')
    return 0;
}
📏相关规则
C++字符数组的读写规则
// 逐个字符读写
char s[100];
cin >> s[0];  // 读入一个字符到 s[0]
cout << s[0]; // 输出 s[0]

// 整串读写(遇到空格/换行停止)
cin >> s;   // 读入一个不含空格的字符串
cout << s;  // 输出整个字符串(遇到\0停止)

// 用 cin.getline(s, 100) 读整行(含空格)
char line[200];
cin.getline(line, 200);  // 读入一整行(含空格)
⚡易混淆概念对比 —— char数组 vs string类
char 数组(C风格字符串)
  • 定义:char s[100] = "Hello";
  • 末尾必须有 '\0' 结尾
  • 长度固定,不能动态扩容
  • 用 strlen() 获取长度
  • 用 strcpy/strcat/strcmp 操作
  • 数组名是常量指针,不能直接赋值
  • sizeof 返回分配的内存大小
string 类(C++风格字符串)
  • 定义:string s = "Hello";
  • 自动管理 '\0',无需关心
  • 长度可变,可以动态扩容
  • 用 .length() 或 .size() 获取长度
  • 用 + 运算符连接,== 比较
  • 可以直接赋值:s = "World";
  • 属于 C++ 标准库,更安全
提示:在 CSP-J / NOIP 比赛中,两种都可以用。char 数组更底层、效率更高;string 类更方便、更安全。本讲重点学习 char 数组,后续课程会深入 string 类。
📚案例详解 —— 由浅入深掌握字符数组操作
案例1统计字符串长度(手动 + strlen)

学习字符数组的第一个操作:统计字符串长度。我们可以用 strlen 函数,也可以手动遍历计数。

C++方法一:使用 strlen
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char s[100] = "Hello, World!";
    int len = strlen(s);
    cout << "字符串: " << s << endl;
    cout << "长度: " << len << endl;
    return 0;
}
C++方法二:手动遍历计数
#include <iostream>
using namespace std;
int main() {
    char s[100] = "Hello, World!";
    int len = 0;
    while (s[len] != '\0') {
        len++;
    }
    cout << "字符串: " << s << endl;
    cout << "长度: " << len << endl;
    return 0;
}
运行结果
字符串: Hello, World!
长度: 13
案例2字符串复制(strcpy)

把源字符串的内容复制到目标数组中。注意:目标数组必须有足够的空间!

C++strcpy 字符串复制
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char src[50] = "Hello";
    char dst[50];
    strcpy(dst, src);
    cout << "源字符串: " << src << endl;
    cout << "目标字符串: " << dst << endl;
    // 手动复制
    char dst2[50];
    int i;
    for (i = 0; src[i] != '\0'; i++) {
        dst2[i] = src[i];
    }
    dst2[i] = '\0';
    cout << "手动复制: " << dst2 << endl;
    return 0;
}
运行结果
源字符串: Hello
目标字符串: Hello
手动复制: Hello
案例3字符串连接(strcat)

把两个字符串拼接到一起。注意:目标数组必须有足够的空间来容纳拼接后的结果!

C++strcat 字符串连接
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char s1[100] = "Hello";
    char s2[50] = " World";
    strcat(s1, s2);
    cout << "拼接结果: " << s1 << endl;
    // 手动连接
    char s3[100] = "Hello";
    char s4[50] = " World";
    int i = 0, j = 0;
    while (s3[i] != '\0') i++;
    while (s4[j] != '\0') {
        s3[i] = s4[j];
        i++; j++;
    }
    s3[i] = '\0';
    cout << "手动拼接: " << s3 << endl;
    return 0;
}
运行结果
拼接结果: Hello World
手动拼接: Hello World
案例4字符串比较(strcmp)

比较两个字符串是否相同。strcmp 按字典序(ASCII码)逐个字符比较。

C++strcmp 字符串比较
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char a[50] = "Hello";
    char b[50] = "Hello";
    char c[50] = "World";
    if (strcmp(a, b) == 0)
        cout << "a 和 b 相同" << endl;
    if (strcmp(a, c) < 0)
        cout << "a 在字典序中小于 c" << endl;
    // 手动比较
    int i;
    for (i = 0; a[i] != '\0' && b[i] != '\0'; i++) {
        if (a[i] != b[i]) break;
    }
    if (a[i] == '\0' && b[i] == '\0')
        cout << "手动比较:a 和 b 相同" << endl;
    return 0;
}
运行结果
a 和 b 相同
a 在字典序中小于 c
案例5密码验证程序

综合运用字符数组和字符串函数,实现一个简单的密码验证程序。

C++密码验证
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char correct[20] = "admin123";
    char input[20];
    int attempts = 3;
    cout << "=== 用户登录系统 ===" << endl;
    while (attempts > 0) {
        cout << "请输入密码 (剩余" << attempts << "次机会): ";
        cin >> input;
        if (strcmp(input, correct) == 0) {
            cout << "登录成功!欢迎回来!" << endl;
            return 0;
        } else {
            attempts--;
            cout << "密码错误,请重试!" << endl;
        }
    }
    cout << "账户已锁定,请稍后再试。" << endl;
    return 0;
}
运行结果(输入正确密码)
=== 用户登录系统 ===
请输入密码 (剩余3次机会): admin123
登录成功!欢迎回来!
运行结果(连续输错3次)
=== 用户登录系统 ===
请输入密码 (剩余3次机会): 123456
密码错误,请重试!
请输入密码 (剩余2次机会): admin
密码错误,请重试!
请输入密码 (剩余1次机会): wrong
密码错误,请重试!
账户已锁定,请稍后再试。
模块 03

实践应用

通过综合题目巩固字符数组的用法,每题包含完整题目、输入输出样例、多种解法和思考。

题目1凯撒密码加密(洛谷 P1059 改编)
CSP-J 难度 字符串 + 字符数组

题目描述

凯撒密码是一种简单的替换加密方法。加密规则是将每个字母向后移动3个位置(a→d, b→e, ..., z→c)。非字母字符保持不变。

输入格式:一行字符串(可能包含大小写字母和空格)

输出格式:加密后的字符串

输入样例:

Attack at dawn

输出样例:

Dwwdfn dw gdzq
题目分析:遍历字符串的每个字符,如果是大写字母(A-Z),则向后移3位(注意Z→C需要回绕);如果是小写字母(a-z),同样处理;其他字符保持不变。
方法一使用字符数组 + ASCII码运算
C++凯撒密码加密
#include <iostream>
using namespace std;
int main() {
    char s[1000];
    cin.getline(s, 1000);  // 读入含空格的整行
    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] >= 'A' && s[i] <= 'Z') {
            s[i] = (char)((s[i] - 'A' + 3) % 26 + 'A');
        } else if (s[i] >= 'a' && s[i] <= 'z') {
            s[i] = (char)((s[i] - 'a' + 3) % 26 + 'a');
        }
    }
    cout << s << endl;
    return 0;
}
方法二使用 cin.getline + 逐字符处理(另一种写法)
C++凯撒密码 - 逐字符处理
#include <iostream>
using namespace std;
int main() {
    char ch;
    while (cin.get(ch) && ch != '\n') {
        if (ch >= 'A' && ch <= 'Z') {
            ch = (char)((ch - 'A' + 3) % 26 + 'A');
        } else if (ch >= 'a' && ch <= 'z') {
            ch = (char)((ch - 'a' + 3) % 26 + 'a');
        }
        cout << ch;
    }
    cout << endl;
    return 0;
}
思考:如果解密呢?只需要把偏移量改为 -3(或 +23)即可。你能写出解密函数吗?凯撒密码在现代密码学中非常不安全,因为密钥只有26种可能,可以被暴力破解。
题目2回文字符串判断
CSP-J 难度 字符串 + 双指针

题目描述

回文是指正读和反读都一样的字符串。例如 "level"、"racecar"、"madam" 都是回文。请编写程序判断一个字符串是否为回文。

输入格式:一行字符串(不含空格,长度不超过1000)

输出格式:如果是回文输出 "Yes",否则输出 "No"

输入样例1:

level

输出样例1:

Yes

输入样例2:

hello

输出样例2:

No
题目分析:用双指针法,一个指针从开头(i=0),一个从结尾(j=len-1),逐个比较对应位置的字符是否相同。如果有不同的就不是回文。
C++回文判断
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char s[1001];
    cin >> s;
    int n = strlen(s);
    int i = 0, j = n - 1;
    bool isPalin = true;
    while (i < j) {
        if (s[i] != s[j]) {
            isPalin = false;
            break;
        }
        i++; j--;
    }
    if (isPalin)
        cout << "Yes" << endl;
    else
        cout << "No" << endl;
    return 0;
}
思考:除了双指针法,还可以用字符数组反转后比较的方法。你能写出反转比较的解法吗?回文判断在字符串处理中非常常见,比如判断身份证号是否回文、判断日期是否回文等。
题目3字符串中元音字母统计
CSP-J 难度 字符串 + 统计

题目描述

给定一个字符串,统计其中元音字母(a, e, i, o, u)的数量(不区分大小写),并输出每个元音字母出现的次数。

输入格式:一行字符串(可能包含大小写字母和空格,长度不超过1000)

输出格式:输出元音字母总数,然后逐行输出每个元音字母的出现次数

输入样例:

Hello World

输出样例:

Total vowels: 3
a: 0
e: 1
i: 0
o: 2
u: 0
C++元音字母统计
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char s[1001];
    cin.getline(s, 1001);
    int a = 0, e = 0, i = 0, o = 0, u = 0;
    for (int j = 0; s[j] != '\0'; j++) {
        char c = tolower(s[j]);
        if (c == 'a') a++;
        else if (c == 'e') e++;
        else if (c == 'i') i++;
        else if (c == 'o') o++;
        else if (c == 'u') u++;
    }
    int total = a + e + i + o + u;
    cout << "Total vowels: " << total << endl;
    cout << "a: " << a << endl;
    cout << "e: " << e << endl;
    cout << "i: " << i << endl;
    cout << "o: " << o << endl;
    cout << "u: " << u << endl;
    return 0;
}
思考:这个题目可以用 switch-case 代替 if-else 链来写,代码会更简洁。另外,可以用一个字符数组存储元音字母,用嵌套循环来统计,减少代码重复。你能尝试优化吗?
模块 04

总结提升

回顾本课核心要点,通过拓展案例加深对字符数组的理解。

📝核心要点总结
知识点语法/函数说明
字符数组定义char s[100] = "Hello";末尾自动加\0
strlenstrlen(s)返回字符串长度(不含\0)
strcpystrcpy(dst, src)复制字符串
strcatstrcat(s1, s2)连接字符串
strcmpstrcmp(s1, s2)比较字符串,0=相同
strchrstrchr(s, ch)查找字符首次出现位置
strstrstrstr(s1, s2)查找子串首次出现位置
sprintfsprintf(s, fmt, ...)格式化写入字符串
sscanfsscanf(s, fmt, ...)从字符串格式化读取
🔑 字符数组万能模板

// 遍历字符数组的标准模板
for (int i = 0; s[i] != '\0'; i++) {
    // 处理 s[i]
}
// 注意:strlen 不计算 \0,sizeof 计算分配的内存大小

📚拓展案例(一题多解)
拓展1字符串逆序(三种方法)

将字符串逆序输出,例如 "Hello" → "olleH"。

方法一倒序遍历输出
C++方法一:倒序遍历
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char s[100] = "Hello";
    int n = strlen(s);
    for (int i = n - 1; i >= 0; i--) {
        cout << s[i];
    }
    cout << endl;
    return 0;
}
方法二原地交换(双指针)
C++方法二:原地交换
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char s[100] = "Hello";
    int n = strlen(s);
    for (int i = 0; i < n / 2; i++) {
        char tmp = s[i];
        s[i] = s[n - 1 - i];
        s[n - 1 - i] = tmp;
    }
    cout << s << endl;
    return 0;
}
方法三使用新数组
C++方法三:新数组
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char s[100] = "Hello";
    char rev[100];
    int n = strlen(s);
    for (int i = 0; i < n; i++) {
        rev[i] = s[n - 1 - i];
    }
    rev[n] = '\0';
    cout << rev << endl;
    return 0;
}
运行结果(三种方法输出相同)
olleH
拓展2统计字符出现次数(两种方法)

统计字符串中每个字符出现的次数。

C++方法一:用数组统计(ASCII映射)
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char s[100] = "Hello World";
    int cnt[256] = {0};  // ASCII 256种字符
    for (int i = 0; s[i] != '\0'; i++) {
        cnt[(unsigned char)s[i]]++;
    }
    for (int i = 0; i < 256; i++) {
        if (cnt[i] > 0)
            cout << (char)i << ": " << cnt[i] << endl;
    }
    return 0;
}
拓展3字符串中数字提取

从混合字符串中提取所有数字字符,组成新的字符串。

C++提取数字
#include <iostream>
using namespace std;
int main() {
    char s[100] = "abc123def456";
    char nums[100];
    int j = 0;
    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] >= '0' && s[i] <= '9') {
            nums[j++] = s[i];
        }
    }
    nums[j] = '\0';
    cout << nums << endl;  // 输出: 123456
    return 0;
}

💭 思考与启发

对比上面的三种逆序方法,你有什么启发?

1. 倒序遍历最简单,但不修改原字符串,只是逆序输出。

2. 原地交换最节省空间,只需要一个临时变量,时间复杂度 O(n/2)。

3. 新数组最直观,但需要额外的 O(n) 空间。

这个代码适合解决什么问题?能否设计一个新算法解决本题?

提示:在嵌入式系统中,空间非常宝贵,原地交换是最优选择。在需要保留原字符串的场景下,倒序遍历或新数组更合适。选择哪种方法取决于具体的使用场景。

模块 05

学习检测

通过典型错误代码,加深对字符数组用法的理解,避免常见陷阱。

⚡5大易错点
ERROR 01

下标越界访问

错误写法
char s[5] = "Hello";
s[5] = 'w';  // 越界!最大下标是4
正确写法
char s[6] = "Hello";
s[5] = 'w';  // 正确,下标5是\0的位置
预防口诀:定义时多留1个位置,存\0不越界。
ERROR 02

未初始化就使用

错误写法
char s[100];
cout << s;  // 未初始化,内容是随机的
正确写法
char s[100] = {0};  // 全部初始化为\0
cin >> s;
cout << s << endl;
预防口诀:定义数组要初始化,{0}清零最安全。
ERROR 03

忘记添加 \0 结束标志

错误写法
char s[5] = {'H','e','l','l','o'};
cout << s;  // 没有\0,输出会越界!
正确写法
char s[6] = {'H','e','l','l','o','\0'};
cout << s;  // 有\0,正常输出
预防口诀:手动初始化要加\0,用字符串初始化不用愁。
ERROR 04

数组名直接赋值

错误写法
char s[100];
s = "Hello";  // 错误!数组名是常量指针
正确写法
char s[100];
strcpy(s, "Hello");  // 正确:用strcpy复制
预防口诀:数组名不能直接赋值,strcpy来帮忙。
ERROR 05

char数组忘 \0 导致 strlen 错误

错误写法
char s[5] = {'A','B','C'};
cout << strlen(s);  // 没有\0,strlen越界查找
正确写法
char s[5] = {'A','B','C','\0','D'};
cout << strlen(s);  // 输出3,遇到\0停止
预防口诀:strlen找\0,没有\0就爆炸。
📋速查表
函数头文件用法返回值
strlen<cstring>strlen(s)字符串长度(不含\0)
strcpy<cstring>strcpy(dst, src)返回dst指针
strcat<cstring>strcat(s1, s2)返回s1指针
strcmp<cstring>strcmp(s1, s2)0=相同, <0=s1<s2, >0=s1>s2
strchr<cstring>strchr(s, ch)返回ch首次出现位置的指针,未找到返回NULL
strstr<cstring>strstr(s1, s2)返回s2在s1中首次出现的位置,未找到返回NULL
sprintf<cstdio>sprintf(s, "fmt", ...)返回写入的字符数
sscanf<cstdio>sscanf(s, "fmt", ...)返回成功读取的变量数
📌知识点速记卡
📌 定义

char s[100] = "Hello"; 末尾自动加\0,长度6

📌 长度

strlen(s) 返回5(不含\0),sizeof(s) 返回100(分配空间)

📌 复制

strcpy(dst, src) 把src复制到dst,dst必须有足够空间

📌 连接

strcat(s1, s2) 把s2拼接到s1末尾,s1必须有足够空间

📌 比较

strcmp(s1, s2) 返回0表示相同,不能用 == 比较

📌 查找

strchr(s, 'a') 找字符,strstr(s, "abc") 找子串

📌 格式化

sprintf(s, "%d", 42) 把42格式化为字符串"42"

📌 读入

cin >> s 不读空格,cin.getline(s, 100) 读整行

模块 06

练习作业

通过分层练习巩固字符数组的用法,从基础到挑战,逐步提升。

📝总结练习(3题)
总结练习 1 字符串 + 统计

统计单词个数

输入一行字符串(可能包含多个空格),统计其中单词的个数。单词之间用一个或多个空格分隔。

输入样例:" Hello World C++ "

输出样例:3

查看参考答案与解析
C++统计单词个数
#include <iostream>
using namespace std;
int main() {
    char s[1000];
    cin.getline(s, 1000);
    int words = 0;
    bool inWord = false;
    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] != ' ' && s[i] != '\t') {
            if (!inWord) {
                words++;
                inWord = true;
            }
        } else {
            inWord = false;
        }
    }
    cout << words << endl;
    return 0;
}

解析:用一个布尔变量 inWord 标记当前是否在单词内部。遇到非空格字符且不在单词内时,单词数+1。

总结练习 2 字符串 + 字符数组

去除字符串中的空格

输入一行字符串,去除其中所有的空格后输出。

输入样例:"Hello World"

输出样例:"HelloWorld"

查看参考答案与解析
C++去除空格
#include <iostream>
using namespace std;
int main() {
    char s[1000];
    cin.getline(s, 1000);
    int j = 0;
    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] != ' ') {
            s[j++] = s[i];
        }
    }
    s[j] = '\0';
    cout << s << endl;
    return 0;
}

解析:用双指针法,j 指向新字符串的写入位置,i 遍历原字符串。遇到非空格字符就复制到 j 位置,最后添加 \0。

总结练习 3 字符串 + 查找

查找子串位置(洛谷 P1059 改编)

输入两个字符串 s1 和 s2,查找 s2 在 s1 中首次出现的位置。如果找不到,输出 -1。

输入样例:s1 = "Hello World", s2 = "World"

输出样例:6

查看参考答案与解析
C++查找子串位置
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char s1[1000], s2[1000];
    cin >> s1 >> s2;
    char *p = strstr(s1, s2);
    if (p != NULL)
        cout << (p - s1) << endl;  // 指针相减得到位置
    else
        cout << -1 << endl;
    return 0;
}

解析:使用 strstr 函数查找子串,返回指向首次出现位置的指针。用指针相减 p - s1 得到从0开始的索引位置。

AA组·基础(4题)
A1 基础

字符串长度统计

输入一个字符串,使用 strlen 函数和手动遍历两种方法分别统计其长度,比较结果是否一致。

查看参考答案
C++字符串长度
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char s[100];
    cin >> s;
    cout << "strlen: " << strlen(s) << endl;
    int len = 0;
    while (s[len] != '\0') len++;
    cout << "手动: " << len << endl;
    return 0;
}

思路:strlen 直接给出 '\0' 前的字符个数,也可以自己用 while 循环数到 '\0' 为止。

易错:strlen 统计的是 '\0' 之前的字符数,不含结尾的 '\0' 本身;字符数组必须以 '\0' 结尾才能用 strlen。

A2 基础

字符翻转输出

输入一个字符串,将其逆序输出。例如输入 "abc",输出 "cba"。

查看参考答案
C++字符翻转
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char s[100];
    cin >> s;
    int n = strlen(s);
    for (int i = n - 1; i >= 0; i--)
        cout << s[i];
    cout << endl;
    return 0;
}

思路:先算出长度 n,从下标 n-1 倒着循环到 0,逐个输出即可实现翻转。

易错:起始下标写成 n 会越界访问 '\0' 后面的内存,应从 n-1 开始。

A3 基础

字符串连接

输入两个字符串,使用 strcat 将它们连接起来并输出结果。

查看参考答案
C++字符串连接
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char s1[200], s2[100];
    cin >> s1 >> s2;
    strcat(s1, s2);
    cout << s1 << endl;
    return 0;
}

思路:strcat(s1, s2) 把 s2 的内容(连同 '\0')接到 s1 末尾,s1 必须足够大。

易错:s1 数组太小装不下拼接结果会导致越界;连接前 s1 必须以 '\0' 结尾。

A4 基础

字符查找

输入一个字符串和一个字符,使用 strchr 查找该字符在字符串中首次出现的位置。

查看参考答案
C++字符查找
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char s[100], ch;
    cin >> s >> ch;
    char *p = strchr(s, ch);
    if (p)
        cout << "找到,位置: " << (p - s) << endl;
    else
        cout << "未找到" << endl;
    return 0;
}

思路:strchr 返回字符首次出现位置的指针,用 p - s 就得到下标。

易错:找不到时 strchr 返回 NULL,必须先判断 p 非空再用 p - s,否则解引用空指针崩溃。

BB组·提高(4题)
B1 提高

字符串去重(洛谷 P1059 改编)

输入一个字符串,去除其中重复的字符,只保留第一次出现的字符。例如 "programming" → "progamin"。

查看参考答案
C++字符串去重
#include <iostream>
using namespace std;
int main() {
    char s[1000];
    cin >> s;
    char result[1000] = {0};
    int j = 0;
    for (int i = 0; s[i] != '\0'; i++) {
        bool found = false;
        for (int k = 0; k < j; k++) {
            if (result[k] == s[i]) {
                found = true;
                break;
            }
        }
        if (!found) result[j++] = s[i];
    }
    result[j] = '\0';
    cout << result << endl;
    return 0;
}

思路:双重循环去重:每个字符先在已保存的结果里找一遍,没出现过才存入结果数组。

易错:内层查找的范围应是 j(已保存的个数),写成整个数组长度会判断出错。

B2 提高

字符串排序(冒泡排序)

输入 n 个字符串(n ≤ 100),按字典序从小到大排序后输出。

查看参考答案
C++字符串排序
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    int n;
    cin >> n;
    char arr[100][100];
    for (int i = 0; i < n; i++)
        cin >> arr[i];
    for (int i = 0; i < n - 1; i++)
        for (int j = 0; j < n - 1 - i; j++)
            if (strcmp(arr[j], arr[j+1]) > 0) {
                char tmp[100];
                strcpy(tmp, arr[j]);
                strcpy(arr[j], arr[j+1]);
                strcpy(arr[j+1], tmp);
            }
    for (int i = 0; i < n; i++)
        cout << arr[i] << endl;
    return 0;
}

思路:对字符数组做冒泡排序,比较用 strcmp,交换整个数组内容用 strcpy。

易错:直接用 arr[i] < arr[j] 比较的是数组地址而不是字符串内容,必须用 strcmp。

B3 提高

字符统计(字母、数字、其他)

输入一行字符串,分别统计其中字母、数字和其他字符的个数。

查看参考答案
C++字符统计
#include <iostream>
using namespace std;
int main() {
    char s[1000];
    cin.getline(s, 1000);
    int letter = 0, digit = 0, other = 0;
    for (int i = 0; s[i] != '\0'; i++) {
        if ((s[i] >= 'A' && s[i] <= 'Z') || (s[i] >= 'a' && s[i] <= 'z'))
            letter++;
        else if (s[i] >= '0' && s[i] <= '9')
            digit++;
        else
            other++;
    }
    cout << "字母: " << letter << endl;
    cout << "数字: " << digit << endl;
    cout << "其他: " << other << endl;
    return 0;
}

思路:遍历每个字符,按大写字母、小写字母、数字的 ASCII 范围分别累加计数器,其余算其他。

易错:含空格的整行要用 cin.getline 读入,cin >> s 遇到空格就停止了。

B4 提高

字符串中数字提取并求和

输入一个字符串,提取其中所有连续的数字组成整数,并求和。例如 "abc123def45" 输出 168(123+45)。

查看参考答案
C++数字提取求和
#include <iostream>
using namespace std;
int main() {
    char s[1000];
    cin.getline(s, 1000);
    int sum = 0, num = 0;
    bool hasNum = false;
    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] >= '0' && s[i] <= '9') {
            num = num * 10 + (s[i] - '0');
            hasNum = true;
        } else {
            if (hasNum) {
                sum += num;
                num = 0;
                hasNum = false;
            }
        }
    }
    if (hasNum) sum += num;
    cout << sum << endl;
    return 0;
}

思路:遇到数字就 num = num*10 + (s[i]-'0') 拼数,遇到非数字把 num 加进 sum 并清零,别忘末尾那一个。

易错:字符转数值要减 '0',直接加 s[i] 会加上 ASCII 码;循环结束后最后一段数字还要再入账一次。

CC组·挑战(4题)
C1 挑战

字符串压缩(洛谷 P1059 改编)

给定一个字符串,进行简单压缩。例如 "aaabbc" → "a3b2c1"。连续相同字符用字符+出现次数表示。

查看参考答案
C++字符串压缩
#include <iostream>
using namespace std;
int main() {
    char s[1000];
    cin >> s;
    int cnt = 1;
    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] == s[i+1]) {
            cnt++;
        } else {
            cout << s[i] << cnt;
            cnt = 1;
        }
    }
    cout << endl;
    return 0;
}

思路:相邻字符相同就计数加一,不同就输出该字符和次数并复位,靠 '\0' 不等于前一个字符来收尾。

易错:循环里访问 s[i+1],最后一组靠 '\0' 触发输出,条件写错就会漏掉最后一组。

C2 挑战

最长回文子串

输入一个字符串,找出其中最长的回文子串并输出其长度和内容。

查看参考答案
C++最长回文子串
#include <iostream>
#include <cstring>
using namespace std;
int main() {
    char s[1000];
    cin >> s;
    int n = strlen(s);
    int bestLen = 1, bestStart = 0;
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j <= n; j++) {
            int len = j - i;
            bool isPalin = true;
            for (int k = 0; k < len / 2; k++) {
                if (s[i+k] != s[j-1-k]) {
                    isPalin = false;
                    break;
                }
            }
            if (isPalin && len > bestLen) {
                bestLen = len;
                bestStart = i;
            }
        }
    }
    char result[1000];
    strncpy(result, s + bestStart, bestLen);
    result[bestLen] = '\0';
    cout << "长度: " << bestLen << endl;
    cout << "内容: " << result << endl;
    return 0;
}

思路:枚举所有子串,用左右双指针向中间比较判断是否回文,记录最长的一个。

易错:判断回文时内层条件写成 l < r 以外的形式容易出错,且别忘了长度为 1 的子串也是回文。

C3 挑战

字符串格式化输出(sprintf 练习)

使用 sprintf 将整数、浮点数、字符串格式化写入字符数组,然后输出。

查看参考答案
C++sprintf 格式化
#include <iostream>
#include <cstdio>
using namespace std;
int main() {
    char buf[200];
    int age = 18;
    double score = 95.5;
    char name[] = "小明";
    sprintf(buf, "姓名: %s, 年龄: %d, 成绩: %.1f", name, age, score);
    cout << buf << endl;
    // 输出: 姓名: 小明, 年龄: 18, 成绩: 95.5
    return 0;
}

思路:sprintf 把格式化的内容写进字符数组 buf,占位符 %s/%d/%.1f 分别对应字符串、整数、一位小数。

易错:buf 数组太小会被写越界;占位符类型要和参数一一对应,用 %d 输出 double 会出错。

C4 挑战

约瑟夫环(字符数组版)

n个人围成一圈,从第1个人开始报数,报到m的人出列,再从下一个人重新开始报数,报到m的人出列,直到所有人都出列。用字符数组模拟这个过程。

查看参考答案
C++约瑟夫环
#include <iostream>
using namespace std;
int main() {
    int n, m;
    cin >> n >> m;
    char people[100];
    for (int i = 0; i < n; i++)
        people[i] = '0' + (i + 1) % 10;  // 存编号字符(n≤9)
    int count = n, pos = 0, step = 0;
    while (count > 0) {
        if (people[pos] != '0') {
            step++;
            if (step == m) {
                cout << people[pos] << " ";
                people[pos] = '0';
                count--;
                step = 0;
            }
        }
        pos = (pos + 1) % n;
    }
    cout << endl;
    return 0;
}

思路:数组存编号,报到 m 的人标记出圈并重新从 1 报数,直到全部出圈。

易错:下标往后走要取模 % n 循环回开头,忘了取模会数组越界。