C++ 核心知识点汇总(第六日)(字符串)

C++ 核心知识点汇总(字符串)

引言

字符串是C++中处理文本数据的核心工具。C++提供了两种字符串处理方式:C风格字符数组和C++标准库string类。其中string类以其便捷的操作、动态扩容和丰富的成员函数,成为现代C++开发的首选。


一、核心:C风格字符数组 vs C++11 std::string类

信奥赛中优先使用std::string类 ,其便捷的操作能大幅减少代码量、降低出错率,仅在极致性能优化(如大字符串频繁操作)场景下少量使用C风格字符数组。

类型 底层实现 竞赛核心特点 适用竞赛场景 竞赛注意事项
C风格字符数组 char[]/char*,以'\0'结尾 手动管理内存,依赖<cstring>库函数,执行效率略高 1. 题目明确要求使用字符数组;2. 大字符串频繁读写的极致优化 1. 必须预留'\0'空间;2. 谨防数组越界(竞赛高频RE原因);
C++11 std::string 封装的动态字符数组,兼容C风格 自动内存管理、支持运算符重载、丰富的成员函数、C++11范围for/auto,无需维护'\0' 99%的竞赛场景,包括模拟、枚举、动态规划、字符串匹配等 1. 下标pos == size()为合法访问,返回'\0';2. size()返回无符号整型,避免与负数比较;

二、C++11 std::string :头文件与初始化

1. 必备头文件与命名空间

竞赛中固定写法,无需额外冗余代码:

cpp 复制代码
#include <iostream>
#include <string> // 字符串核心头文件,必加
using namespace std; // 竞赛中直接使用,减少代码量

注意:无需额外包含<cstring>,除非使用C风格字符数组的库函数(如strlen/strcat)。

2. C++11 推荐初始化方式

竞赛中优先选择简洁、高效的初始化方式,避免冗余,以下为考纲要求掌握的写法(按竞赛使用频率排序):

cpp 复制代码
string s1;                // 空字符串,竞赛中最常用(后续动态赋值)
string s2 = "GESP CSP";   // 直接初始化字符串常量,竞赛高频
string s3("123456");      // 构造函数初始化,与s2等价
string s4{7, 'a'};        // C++11列表初始化,生成7个'a':"aaaaaaa"(竞赛模拟题高频)
string s5 = s2;           // 拷贝初始化,适用于字符串复制
// C++11移动初始化:竞赛中大字符串拷贝时使用,提升效率
string temp = "long string";
string s6 = move(temp);   // 转移资源,无拷贝开销,竞赛性能优化点

避坑:禁止使用string s = NULL;,C++11中NULL为指针,与字符串类型不匹配,直接编译错误。


三、C++11 std::string :输入与输出

字符串的输入输出是入门必考考点 ,也是高频错题点,需严格区分无空格输入带空格输入

1. 无空格输入:cin >> s

竞赛最常用 ,自动跳过空格、回车、Tab等空白字符,遇到空白字符停止读取,适用于读取单个无空格字符串(如单词、数字串)。

cpp 复制代码
string s;
cin >> s; // 输入:csp_j 2025
cout << s; // 输出:csp_j(仅读取第一个空白符前的内容)

竞赛真题场景:读取单个单词、数字字符串(如"123456")、竞赛指令码(如"U D L R")。

2. 带空格输入:getline(cin, s)

适用于读取整行带空格的字符串 (如竞赛模拟题中的整行指令、句子),必须处理输入缓冲区残留的换行符(高频错题点)。

正确写法(竞赛标准)
cpp 复制代码
#include <iostream>
#include <string>
#include <limits> // 必须包含,用于numeric_limits
using namespace std;

int main() {
    int n;
    cin >> n; // 读取整数后,缓冲区残留换行符
    // 清空缓冲区所有字符,直到遇到换行符(竞赛标准写法,避免getline读空串)
    cin.ignore(numeric_limits<streamsize>::max(), '\n');
    
    string s;
    getline(cin, s); // 输入:CSP-S 2025 Round 1
    cout << s; // 输出:CSP-S 2025 Round 1(完整读取整行)
    return 0;
}

避坑:若未使用cin.ignoregetline会直接读取缓冲区的换行符,得到空字符串,导致答案错误(WA)。

3. 进阶:逐个字符输入

适用于需要过滤字符的场景(如读取字符串并过滤非数字/非字母字符):

cpp 复制代码
string s;
char c;
while (cin >> c) { // 逐个读取字符,自动跳过空白符
    if (c >= '0' && c <= '9') { // 仅保留数字字符
        s += c;
    }
}
cout << s; // 输入:a1b2c3,输出:123

四、C++11 std::string 成员函数(高频)

信奥赛中对string类的考查集中在核心成员函数 ,要求选手熟记函数参数、返回值、使用场景,能快速实现字符串的长度获取、拼接、替换、查找、截取等操作。

1. 长度获取:size() / length()

  • 说明 :两者功能完全一致,均返回字符串有效字符数 (不含内部维护的'\0'),返回值类型为size_t(无符号整型,竞赛中注意与int的转换)。
  • 推荐size(),更贴合STL容器命名规范,竞赛中使用频率更高。
  • 真题场景:遍历字符串、判断字符串长度是否符合条件。
cpp 复制代码
string s = "csp";
cout << s.size(); // 输出:3
// 竞赛遍历写法(标准)
for (int i = 0; i < (int)s.size(); i++) { // 强转int,避免无符号整型负数比较坑
    cout << s[i] << " ";
}

竞赛避坑:size()返回size_t(无符号),若直接与负数比较(如i > s.size()-5),会因隐式转换导致逻辑错误,竞赛中统一强转为int

2. 字符串拼接:+ 运算符 / append()

  • 说明+运算符更简洁,竞赛中首选append()适用于批量追加字符,性能略高。
  • 场景:字符串拼接、动态生成字符串。
cpp 复制代码
string s1 = "CSP-", s2 = "S";
string s3 = s1 + s2 + " 2025"; // 首选,简洁
cout << s3; // 输出:CSP-S 2025
s3.append(3, '!'); // 追加3个'!'
cout << s3; // 输出:CSP-S 2025!!!

3. 字符串替换:replace(pos, len, str)

  • 函数参数pos(起始替换下标)、len(待替换字符数)、str(替换的新字符串)。
  • 真题场景:字符串修改、模拟题中的字符替换(如将指定子串替换为其他字符)。
cpp 复制代码
// 竞赛真题:将字符串中从下标1开始的5个字符替换为"12345"
string s = "1234@@chenadai";
s.replace(1, 5, "12345");
cout << s; // 输出:112345chenadai

4. 子串查找:find(sub, pos)

  • 函数参数sub(待查找的子串/字符)、pos(起始查找下标,默认0)。
  • 返回值 :找到则返回子串起始下标 ,找不到返回string::npos(竞赛必背,代表查找失败,值为-1)。
  • 真题场景:字符串分割、判断子串是否存在、查找字符出现位置(竞赛高频)。
cpp 复制代码
string s = "gesp.ccf.org.cn";
size_t pos = s.find('.'); // 查找第一个'.'的位置
if (pos != string::npos) { // 标准判断:先检查是否找到
    cout << pos; // 输出:4
}
pos = s.find('.', pos + 1); // 从下一个位置继续查找
cout << pos; // 输出:8

竞赛必背:string::npos是查找失败的标志,必须先判断是否找到,再使用返回的下标,否则会因下标越界导致RE。

5. 子串截取:substr(pos, len)

  • 函数参数pos(起始截取下标)、len(截取长度,缺省时默认截取至字符串末尾)。
  • 真题场景:字符串分割、截取指定位置的子串(如从域名中截取主域名、从数字串中截取指定位数)。
cpp 复制代码
string s = "gesp.ccf.org.cn";
size_t pos = s.find('.');
string sub = s.substr(0, pos); // 从0开始,截取pos个字符
cout << sub; // 输出:gesp
sub = s.substr(pos + 1); // 从pos+1开始,截取至末尾
cout << sub; // 输出:ccf.org.cn

竞赛注意:若len超过字符串剩余长度,自动截取至末尾,不会报错,可放心使用。

6. 字符清空:clear() / erase()

  • clear() :清空所有字符,size()变为0,竞赛中最常用。
  • erase(pos, len) :删除从pos开始的len个字符,适用于删除指定子串。
  • 真题场景:多次使用同一个字符串变量、过滤字符串中的指定子串。
cpp 复制代码
string s = "csp_j_s";
s.erase(3, 2); // 删除从下标3开始的2个字符("_j")
cout << s; // 输出:csp_s
s.clear();
cout << s.size(); // 输出:0

五、核心操作:字符访问与遍历

字符串的遍历和字符访问是基础操作 ,要求选手掌握多种遍历方式,并能根据场景选择最优写法(如修改字符、只读遍历),同时严格规避下标越界问题(竞赛RE高频原因)。

1. 字符访问:operator[]at()(竞赛核心区分)

根据C++11标准,结合竞赛考法,两者的核心区别为竞赛必背内容:

访问方式 边界检查 pos == size() 竞赛场景 竞赛优劣势
s[pos] 合法,返回'\0' 99%的竞赛场景,包括遍历、字符修改 优势:执行效率高、代码简洁;劣势:无边界检查,需手动保证下标合法
s.at(pos) 抛出std::out_of_range异常 调试代码、需要严格边界检查的场景 优势:避免越界RE;劣势:执行效率低,竞赛中极少使用

结论:优先使用s[pos] ,竞赛中通过逻辑保证下标合法(如i < (int)s.size()),无需使用at()(效率低且代码冗余)。

竞赛合法访问示例(贴合C++11标准)
cpp 复制代码
string s = "hello";
cout << s[0]; // 合法,输出:h
cout << s[4]; // 合法,输出:o
cout << s[5]; // 合法(pos == size()),返回'\0',竞赛中可用于判断字符串结束
// cout << s[6]; // 非法(pos > size()),下标越界

2. 遍历方式

方式1:下标遍历(最常用)

适用于需要修改字符需要使用下标的场景(如字符替换、对称判断),标准写法:

cpp 复制代码
string s = "gEsP";
for (int i = 0; i < (int)s.size(); i++) {
    if (s[i] >= 'a' && s[i] <= 'z') {
        s[i] -= 32; // 小写转大写,竞赛高频操作
    }
}
cout << s; // 输出:GESP
方式2:C++11 范围for循环

适用于只读遍历简单修改字符的场景,代码简洁,不易出错,竞赛中高频使用:

cpp 复制代码
string s = "csp2025";
// 只读遍历
for (char c : s) {
    if (c >= '0' && c <= '9') {
        cout << c;
    }
}
// 修改遍历(加引用&),竞赛标准
for (char& c : s) {
    c = toupper(c); // 转大写,需包含<cctype>
}
方式3:C++11 迭代器遍历(结合STL算法)

适用于使用STL算法 的场景(如sort/find):

cpp 复制代码
#include <algorithm> // 必加STL头文件
string s = "bac";
sort(s.begin(), s.end()); // 排序字符串,竞赛高频
cout << s; // 输出:abc
// 迭代器遍历
for (auto it = s.begin(); it != s.end(); it++) {
    cout << *it << " ";
}

竞赛必背:s.begin()返回字符串首字符迭代器,s.end()返回字符串末尾后一个位置的迭代器(不指向有效字符)。


六、高频真题解析(字符串核心)

真题1:下标边界访问与空字符判断(GESP原题)

题目

执行以下代码,输出结果为?

cpp 复制代码
string ch = "hello";
if (ch[5] == NULL) {
    cout << "right" << endl;
} else if (ch[5] == '\0') {
    cout << "wrong" << endl;
} else {
    cout << "hello" << endl;
}
解析(C++11标准)
  1. ch.size() = 5ch[5]pos == size()合法访问 ,固定返回'\0'(ASCII码=0);
  2. NULL在C++11中为整数0,与'\0'隐式转换后相等,第一个条件成立;
  3. if-else if为互斥分支,第一个条件满足后,后续分支不执行。
标准答案
复制代码
right

真题2:小写转大写

题目

将字符串"gEsP is Interesting"中的所有小写字母转为大写,输出结果。

标准写法
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main() {
    string str = "gEsP is Interesting";
    for (char& c : str) { // 引用修改原字符
        if (c >= 'a' && c <= 'z') {
            c += 'A' - 'a'; // 竞赛核心:ASCII偏移转换,无需库函数
        }
    }
    cout << str; // 输出:GESP IS INTERESTING
    return 0;
}
竞赛得分点
  1. 使用范围for循环(C++11标准);
  2. 直接通过ASCII偏移转换,不使用toupper(避免忘记包含<cctype>);
  3. 字符修改加引用&(否则仅修改临时变量,答案错误)。

真题3:字符串分割(按指定字符分割,CSP-J高频)

题目

将字符串"gesp.ccf.org.cn".分割,输出每个子串(竞赛模拟题)。

标准写法(find+substr)
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main() {
    string s = "gesp.ccf.org.cn";
    string delimiter = ".";
    size_t pos = 0;
    while ((pos = s.find(delimiter)) != string::npos) { // 竞赛标准循环
        string token = s.substr(0, pos);
        cout << token << endl;
        s = s.substr(pos + delimiter.size()); // 截取剩余字符串
    }
    cout << s; // 输出最后一个子串
    return 0;
}
得分点
  1. 先判断find的返回值是否为string::npos,再使用下标;
  2. 截取剩余字符串时,跳过分隔符(pos + delimiter.size());
  3. 循环结束后输出最后一个子串(无分隔符的部分)。
相关推荐
小糯米6012 小时前
C++顺序表和vector
开发语言·c++·算法
独望漫天星辰2 小时前
C++ 多态深度解析:从语法规则到底层实现(附实战验证代码)
开发语言·c++
We་ct2 小时前
LeetCode 56. 合并区间:区间重叠问题的核心解法与代码解析
前端·算法·leetcode·typescript
Lionel6892 小时前
分步实现 Flutter 鸿蒙轮播图核心功能(搜索框 + 指示灯)
算法·图搜索算法
小妖6662 小时前
js 实现快速排序算法
数据结构·算法·排序算法
xsyaaaan2 小时前
代码随想录Day30动态规划:背包问题二维_背包问题一维_416分割等和子集
算法·动态规划
王老师青少年编程3 小时前
2024年信奥赛C++提高组csp-s初赛真题及答案解析(阅读程序第3题)
c++·题解·真题·csp·信奥赛·csp-s·提高组
凡人叶枫3 小时前
C++中输入、输出和文件操作详解(Linux实战版)| 从基础到项目落地,避坑指南
linux·服务器·c语言·开发语言·c++
CSDN_RTKLIB3 小时前
使用三方库头文件未使用导出符号情景
c++