【C++ string 类实战指南】:从接口用法到 OJ 解题的全方位解析
一篇吃透 string 常用接口、C++11 简化技巧与编译器差异的深度教程 ✨
💬 前言
用 C 语言处理字符串时,你是否曾为strcpy的越界风险、strlen的重复计算、手动管理字符数组内存而头疼?在 OJ 题中,是否因频繁处理字符串细节而耽误解题思路?
其实 C++ 的string类早已封装了这些复杂操作,它不仅能自动管理内存,还提供了丰富的接口简化字符串处理。但很多开发者只停留在 “用 string 存字符串” 的层面,没吃透其核心接口的设计逻辑,遇到稍复杂的场景就频繁踩坑(如容量浪费、遍历效率低、接口误用)。
本篇文章将从 “实战需求” 出发,结合 C++11 语法与 OJ 例题,带你彻底掌握string类 —— 不仅教你 “怎么用接口”,更帮你理解 “为什么这么用”,让字符串处理从 “麻烦事” 变成 “顺手活”。
✨ 阅读后,你将掌握:
- string 核心接口的使用场景与避坑点(构造、容量、访问、修改);
- auto 与范围 for 如何简化 string 的遍历与操作;
- VS 与 G++ 下 string 的底层差异(小字符串优化、写时拷贝);
- 用 string 接口高效解决 OJ 字符串题的思路与技巧。
文章目录
- 一、为什么要学 string 类?告别 C 语言字符串的 “手动时代”
- 1. C 语言字符串的 3 大痛点
- 2. string 类的核心优势
- 二、C++11 简化技巧:auto 与范围 for 让 string 操作更简洁
- 1. auto:自动推导类型,告别复杂声明
- 2. 范围 for:自动遍历,无需关心下标或迭代器
- 三、string 类常用接口实战:从基础到进阶
- 1. 字符串构造:3 种核心方式
- 2. 容量操作:避免空间浪费与频繁扩容
- 3. 访问与遍历:3 种常用方式
- 4. 修改操作:拼接、查找、截取的高频接口
- 5. 非成员函数:输入输出与比较
- 四、编译器差异:VS 与 G++ 下的 string 底层结构
- 1. VS 下的 string:小字符串优化(SSO)
- 2. G++ 下的 string:写时拷贝(Copy-On-Write)
- 五、OJ 实战:用 string 接口解决 4 道经典字符串题
- 1. 仅反转字母(LeetCode 917)
- 2. 找第一个只出现一次的字符(LeetCode 387)
- 3. 验证回文串(LeetCode 125)
- 4. 字符串相加(LeetCode 415)
- 六、思考与总结 ✨
- 七、自测题与答案解析 🧩
- 八、延伸阅读推荐
- 九、下篇预告:C++ string 类模拟实现 —— 揭开底层内存管理的面纱
一、为什么要学 string 类?告别 C 语言字符串的 “手动时代”
C 语言中,字符串是 “以\0结尾的字符数组”,搭配str系列函数(strcpy、strlen、strcmp)使用,但这种方式存在明显缺陷:
1. C 语言字符串的 3 大痛点
- 数据与操作分离:字符串(字符数组)和操作函数(
strcpy)是分开的,不符合面向对象思想,且容易遗漏操作(如忘记strlen计算长度); - 内存需手动管理:动态申请的字符数组(
char* p = (char*)malloc(...))需手动free,稍不注意就会内存泄漏; - 越界风险高:
strcpy不检查目标数组大小,若源字符串过长,直接导致内存越界,引发程序崩溃。
2. string 类的核心优势
- 自动化内存管理:无需手动
malloc/free,string会自动处理空间申请与释放; - 丰富的接口:内置构造、容量控制、查找、修改等接口,无需重复实现基础功能;
- OJ 与工作刚需:OJ 中 90% 以上的字符串题以
string为输入输出,工作中用string能大幅提升开发效率,极少有人再用 C 语言字符串函数。
二、C++11 简化技巧:auto 与范围 for 让 string 操作更简洁
在学习string接口前,先掌握两个 C++11 语法 ——auto和范围 for,它们能大幅简化string的遍历与迭代器操作,避免冗长代码。
1. auto:自动推导类型,告别复杂声明
auto会让编译器在编译时自动推导变量类型,尤其适合string迭代器这类 “长类型名” 的场景:
#include <iostream>
#include <string>
#include <map>
using namespace std;int main() {// 场景1:简化string迭代器声明string str = "hello world";// 传统写法:string::iterator it = str.begin();auto it = str.begin(); // auto自动推导为string::iteratorwhile (it != str.end()) {cout << *it << " "; // 输出:h e l l o w o r l d++it;}cout << endl;// 场景2:简化复杂容器的迭代器(结合string使用)map<string, string> dict = {{"apple", "苹果"}, {"orange", "橙子"}};// 传统写法:map<string, string>::iterator dictIt = dict.begin();auto dictIt = dict.begin();while (dictIt != dict.end()) {cout << dictIt->first << ":" << dictIt->second << endl;++dictIt;}return 0;
}
auto 使用注意事项
- 声明引用需加
&:auto& ref = str(ref是str的引用,修改ref会改变str); - 同一行声明的变量类型需一致:
auto a = 1, b = 2(正确),auto c = 3, d = 4.0(错误,int 与 double 冲突); - 不能直接声明数组:
auto arr[] = {1,2,3}(编译报错,数组类型不能用 auto 推导)。
2. 范围 for:自动遍历,无需关心下标或迭代器
范围 for 是专门为 “有范围的集合”(如string、数组、容器)设计的遍历方式,自动迭代、自动取数据、自动判断结束,语法简洁且不易出错:
#include <iostream>
#include <string>
using namespace std;int main() {// 场景1:遍历string并修改字符(需加&,否则是值拷贝)string str = "hello";for (auto& ch : str) { // &表示引用,修改ch即修改str的字符ch -= 32; // 小写转大写}cout << str << endl; // 输出:HELLO// 场景2:只读遍历string(无需加&)for (auto ch : str) {cout << ch << " "; // 输出:H E L L O}cout << endl;// 对比C++98的遍历方式(繁琐且易出错)string oldStr = "world";for (int i = 0; i < oldStr.size(); ++i) {cout << oldStr[i] << " "; // 输出:w o r l d}return 0;
}
范围 for 的底层逻辑
范围 for 遍历string时,底层会自动转换为 “迭代器遍历”(从begin()到end()),汇编层面可验证这一点 —— 它本质是迭代器的 “语法糖”,但代码简洁度大幅提升。
三、string 类常用接口实战:从基础到进阶
string的接口众多,我们聚焦 “最常用、最高频” 的接口,按 “构造→容量→访问→修改” 的逻辑拆解,每个接口搭配代码示例与使用场景。
1. 字符串构造:3 种核心方式
string的构造函数能满足不同初始化需求,重点掌握以下 3 种:

构造函数使用示例
#include <iostream>
#include <string>
using namespace std;void TestStringConstructor() {string s1; // 空字符串,size=0,capacity根据编译器默认值(如VS下为0)cout << "s1 size: " << s1.size() << ", empty: " << s1.empty() << endl;string s2("hello bit"); // 用C风格字符串构造cout << "s2: " << s2 << ", size: " << s2.size() << endl;string s3(s2); // 拷贝构造cout << "s3: " << s3 << ", address diff: " << &s2 << " vs " << &s3 << endl;// 注:s2和s3是不同对象,地址不同,底层为深拷贝
}int main() {TestStringConstructor();return 0;
}
2. 容量操作:避免空间浪费与频繁扩容
容量相关接口是string效率优化的关键,核心是 “合理控制空间,减少扩容开销”:

容量接口使用示例(含效率优化)
#include <iostream>
#include <string>
using namespace std;void TestStringCapacity() {string s;// 场景1:reserve预留空间,避免频繁扩容s.reserve(100); // 提前预留100个字符空间for (int i = 0; i < 50; ++i) {s += 'a'; // 无需扩容,效率高}cout << "s size: " << s.size() << ", capacity: " << s.capacity() << endl; // size=50, capacity=100// 场景2:resize修改有效字符数s.resize(80, 'b'); // 有效字符数从50扩到80,新增的30个字符为'b'cout << "s after resize(80, 'b'): " << s << endl;cout << "s size: " << s.size() << ", capacity: " << s.capacity() << endl; // size=80, capacity=100s.resize(30); // 有效字符数从80缩到30,截断后面50个字符cout << "s after resize(30): " << s << endl;cout << "s size: " << s.size() << ", capacity: " << s.capacity() << endl; // size=30, capacity=100// 场景3:clear清空内容s.clear();cout << "s after clear: size=" << s.size() << ", capacity=" << s.capacity() << endl; // size=0, capacity=100
}int main() {TestStringCapacity();return 0;
}
💡 效率优化建议:
如果能预估string的最终长度(如读取固定格式的日志、拼接已知长度的字符串),先用reserve(n)预留空间,可避免string自动扩容时的 “申请新空间→拷贝旧内容→释放旧空间” 操作,大幅提升效率。
3. 访问与遍历:3 种常用方式
string提供了多种访问字符的方式,根据场景选择最合适的:

访问与遍历示例对比
#include <iostream>
#include <string>
using namespace std;void TestStringAccess() {string str = "hello string";// 方式1:operator[](随机访问+修改)str[0] = 'H'; // 修改第一个字符为大写cout << "方式1(operator[]): ";for (size_t i = 0; i < str.size(); ++i) {cout << str[i] << " ";}cout << endl;// 方式2:迭代器(通用遍历,支持反向遍历)cout << "方式2(迭代器): ";auto it = str.begin();while (it != str.end()) {cout << *it << " ";++it;}cout << endl;// 方式3:范围for(最简洁)cout << "方式3(范围for): ";for (auto ch : str) {cout << ch << " ";}cout << endl;
}int main() {TestStringAccess();return 0;
}
输出结果:
方式1(operator[]): H e l l o s t r i n g
方式2(迭代器): H e l l o s t r i n g
方式3(范围for): H e l l o s t r i n g
4. 修改操作:拼接、查找、截取的高频接口
string的修改接口是 OJ 题和工作中的核心,重点掌握以下 5 个:

修改接口实战示例(含 OJ 常用逻辑)
#include <iostream>
#include <string>
using namespace std;void TestStringModify() {string str = "hello";// 1. 追加操作:+=最灵活str += " world"; // 追加字符串str += '!'; // 追加单个字符cout << "追加后:" << str << endl; // 输出:hello world!// 2. 查找操作:find找字符或子串size_t pos1 = str.find('w'); // 找字符'w'的位置if (pos1 != string::npos) {cout << "'w'的位置:" << pos1 << endl; // 输出:6}size_t pos2 = str.find("world"); // 找子串"world"的位置if (pos2 != string::npos) {cout << "\"world\"的位置:" << pos2 << endl; // 输出:6}// 3. 截取操作:substr截取子串string sub = str.substr(pos2, 5); // 从pos2开始,截取5个字符cout << "截取的子串:" << sub << endl; // 输出:world// 4. 兼容C语言:c_str()返回const char*printf("C风格输出:%s\n", str.c_str()); // 输出:hello world!
}int main() {TestStringModify();return 0;
}
💡 OJ 高频技巧:
find与substr结合可实现 “分割字符串”(如按逗号分割),示例逻辑:
string s = "a,b,c,d";
size_t start = 0;
size_t pos = s.find(',');
while (pos != string::npos) {string part = s.substr(start, pos - start); // 截取从start到pos的子串cout << part << endl;start = pos + 1;pos = s.find(',', start); // 从start后继续找逗号
}
string lastPart = s.substr(start); // 截取最后一个子串
cout << lastPart << endl;
5. 非成员函数:输入输出与比较
string的非成员函数主要用于输入输出和字符串比较,用法直观:

非成员函数使用示例(重点:getline 的正确用法)
#include <iostream>
#include <string>
using namespace std;void TestStringIO() {// 注意:cin >> str后会留下换行符,需用cin.ignore()清除string str1;cout << "输入str1(不含空格):";cin >> str1; // 输入:hellocin.ignore(); // 清除cin留下的换行符,否则getline会读取空字符串string str2;cout << "输入str2(可含空格):";getline(cin, str2); // 输入:hello worldcout << "str1: " << str1 << ", size: " << str1.size() << endl;cout << "str2: " << str2 << ", size: " << str2.size() << endl;// 字符串比较if (str1 < str2) {cout << "str1 < str2" << endl;} else if (str1 == str2) {cout << "str1 == str2" << endl;} else {cout << "str1 > str2" << endl;}
}int main() {TestStringIO();return 0;
}
四、编译器差异:VS 与 G++ 下的 string 底层结构
不同编译器对string的实现不同,核心差异在于 “空间存储策略”,了解这一点可避免跨平台开发时的效率问题。
1. VS 下的 string:小字符串优化(SSO)
VS 的string对象占 28 字节,内部用联合体存储字符串:
- 当字符串长度≤15 时:使用内部固定的 16 字节字符数组(
_Buf[16])存储,无需申请堆空间,效率极高; - 当字符串长度≥16 时:从堆上申请空间,用指针(
_Ptr)指向堆空间。
这种设计的优势是 —— 大多数场景下字符串长度较短(如变量名、日志信息),无需堆申请,减少内存开销与访问延迟。
2. G++ 下的 string:写时拷贝(Copy-On-Write)
G++ 的string对象仅占 4 字节(32 位平台),内部只有一个指针,指向堆上的一块空间,该空间包含 3 个核心字段:
_M_length:有效字符长度;
-_M_capacity:总空间大小;_M_refcount:引用计数(记录使用该堆空间的string对象个数)。
“写时拷贝” 的逻辑是:
- 读取时:多个
string对象共享同一块堆空间(引用计数递增); - 修改时:若引用计数 > 1,先拷贝一块新空间,再修改新空间(避免影响其他对象),引用计数调整。
💡 注意:写时拷贝在多线程环境下可能存在线程安全问题,C++11 后部分 G++ 版本已弃用,改用类似 VS 的小字符串优化。
五、OJ 实战:用 string 接口解决 4 道经典字符串题
掌握接口后,通过 OJ 题巩固用法,以下 4 道题是面试高频题,均用string接口高效实现。
1. 仅反转字母(LeetCode 917)
题目: 给定一个字符串,反转其中所有字母,非字母字符位置不变。
思路: 双指针(左指针找字母,右指针找字母,交换后移动指针)。
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;class Solution {
public:bool isLetter(char ch) {return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');}string reverseOnlyLetters(string S) {if (S.empty()) return S;size_t left = 0, right = S.size() - 1;while (left < right) {// 左指针找字母while (left < right && !isLetter(S[left])) ++left;// 右指针找字母while (left < right && !isLetter(S[right])) --right;// 交换字母swap(S[left], S[right]);++left;--right;}return S;}
};int main() {Solution sol;string s = "a-bC-dEf-ghIj";cout << sol.reverseOnlyLetters(s) << endl; // 输出:j-Ih-gfE-dCbareturn 0;
}
2. 找第一个只出现一次的字符(LeetCode 387)
题目:给定一个字符串,找到第一个只出现一次的字符,返回其下标;若无,返回 - 1。
思路:用数组统计字符出现次数(256 个 ASCII 码),再遍历字符串找第一个次数为 1 的字符。
#include <iostream>
#include <string>
using namespace std;class Solution {
public:int firstUniqChar(string s) {int count[256] = {0}; // 统计每个字符出现次数// 第一步:统计次数for (auto ch : s) {count[ch]++;}// 第二步:找第一个次数为1的字符for (int i = 0; i < s.size(); ++i) {if (count[s[i]] == 1) {return i;}}return -1;}
};int main() {Solution sol;string s = "loveleetcode";cout << sol.firstUniqChar(s) << endl; // 输出:2(字符'v'的下标)return 0;
}
3. 验证回文串(LeetCode 125)
题目:给定一个字符串,验证它是否是回文串(只考虑字母和数字字符,忽略大小写)。
思路:双指针(左指针找字母 / 数字,右指针找字母 / 数字,转大写后比较)。
#include <iostream>
#include <string>
using namespace std;class Solution {
public:bool isLetterOrNum(char ch) {return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');}bool isPalindrome(string s) {// 统一转为大写for (auto& ch : s) {if (ch >= 'a' && ch <= 'z') {ch -= 32;}}int left = 0, right = s.size() - 1;while (left < right) {// 左指针找字母/数字while (left < right && !isLetterOrNum(s[left])) ++left;// 右指针找字母/数字while (left < right && !isLetterOrNum(s[right])) --right;// 比较if (s[left] != s[right]) {return false;}++left;--right;}return true;}
};int main() {Solution sol;string s = "A man, a plan, a canal: Panama";cout << (sol.isPalindrome(s) ? "是回文串" : "不是回文串") << endl; // 输出:是回文串return 0;
}
4. 字符串相加(LeetCode 415)
题目:给定两个非负整数的字符串表示(如 “123”+“456”),返回它们的和的字符串表示。
思路:双指针从后往前加,记录进位,结果尾插后反转(避免头插效率低)。
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;class Solution {
public:string addStrings(string num1, string num2) {int i = num1.size() - 1, j = num2.size() - 1;int carry = 0; // 进位string res;while (i >= 0 || j >= 0 || carry > 0) {// 取当前位的数字(越界则为0)int val1 = (i >= 0) ? (num1[i] - '0') : 0;int val2 = (j >= 0) ? (num2[j] - '0') : 0;// 计算当前位和int sum = val1 + val2 + carry;carry = sum / 10; // 更新进位res += (sum % 10) + '0'; // 尾插当前位字符// 移动指针if (i >= 0) --i;if (j >= 0) --j;}reverse(res.begin(), res.end()); // 反转结果return res;}
};int main() {Solution sol;string num1 = "123", num2 = "456";cout << sol.addStrings(num1, num2) << endl; // 输出:579return 0;
}
六、思考与总结 ✨

💡 一句话总结:
string类的核心价值是 “自动化内存管理 + 丰富接口”,掌握reserve的效率优化、find + substr的子串操作、双指针的遍历思路,就能轻松应对 90% 以上的字符串场景。
七、自测题与答案解析 🧩
- 判断题:
string的clear()接口会释放底层空间吗?
❌ 不会。clear()仅清空有效字符(将size设为 0),capacity保持不变,底层空间仍存在。
- 选择题:下列关于
reserve和resize的说法错误的是( )
A.reserve(n)会预留 n 个字符的空间,不改变size
B.resize(n)会将size改为 n,可能改变capacity
C.reserve(n)若 n 小于当前capacity,会缩小空间
D.resize(n, 'a')会将新增字符填充为 ‘a’
答案:✅ C。reserve(n)仅在 n 大于当前capacity时扩容,n 小于时不做任何操作。
- 简答题:为什么
string的+=比operator+效率高?
答案:operator+是传值返回,会创建新的string对象(深拷贝);+=是在原对象上直接追加,无需创建新对象,效率更高。
八、延伸阅读推荐
📗 建议阅读顺序
- 《C++ 内存管理、模板初阶与 STL 简介》
- 《C++ string 类实战指南:从接口用法到 OJ 解题》(本文)
- 《C++ string 类模拟实现:从浅拷贝到深拷贝》(下篇)
九、下篇预告:C++ string 类模拟实现 —— 揭开底层内存管理的面纱
学会string的接口用法后,你是否好奇:
string的深拷贝是如何实现的?为什么浅拷贝会导致程序崩溃?string的扩容机制是怎样的(VS 下 1.5 倍扩容,还是 G++ 下 2 倍扩容)?- 如何自己实现一个支持构造、拷贝构造、赋值重载的简易
string类?
下一篇《C++ string 类模拟实现》将带你深入string的底层,从 “浅拷贝陷阱” 讲到 “深拷贝的两种实现(传统版 + 现代版)”,再到 “扩容逻辑与内存释放”,让你不仅 “会用string”,更 “懂string的底层实现”。
✨ 敬请期待,我们将从 “接口用法” 走向 “底层原理”,彻底掌握string的设计逻辑与内存管理技巧。
🖋 作者寄语
string看似简单,却是 C++ 中 “封装思想” 的典型体现 —— 它把复杂的内存管理、字符操作隐藏在接口背后,让开发者专注业务逻辑。学习string不仅是掌握一个工具,更是理解 “如何用面向对象思想解决实际问题” 的过程。

