💬 :如果你在阅读过程中有任何疑问或想要进一步探讨的内容,欢迎在评论区畅所欲言!我们一起学习、共同成长~!
👍 :如果你觉得这篇文章还不错,不妨顺手点个赞、加入收藏,并分享给更多的朋友噢~!
1. 为什么必须掌握 string 类?
1.1 C 语言字符串的局限性
C 语言中,字符串被定义为以'\0'结尾的字符数组,依赖 str 系列库函数(如 strcpy)。存在显著缺陷:
- 不符合面向对象编程(OOP)思想:C 标准库提供的
str系列函数是独立于字符串数据结构之外的全局函数,无法直接通过对象调用,与 OOP 中 “数据与操作绑定” 的核心思想相悖。 - 内存管理风险高:字符串底层空间需用户手动管理,若操作不当极易引发内存泄漏等安全问题。
- 无法直接支持字符串拼接、查找等常用操作,需手动实现。
1.2 面试笔试中的核心地位
- OJ 题目中字符串类题目 90% 以上以
string类形式出现,极少使用 C 库函数; - 面试高频要求模拟实现
string类(构造、拷贝构造、赋值运算符重载、析构),考查深拷贝能力; - 常规工作中
string类是字符串操作的首选,是基础工具类的核心。
2.string类的核心接口(必须掌握)
使用 string 类需:
- 包含头文件<string>
- 声明 using namespace std; 或 std::string
2.1 构造函数
| 构造函数原型 | 功能描述 |
|---|---|
string()(重点) | 默认构造函数,创建一个空的string对象。 |
string(const char* s)(重点) | 用 C 风格字符串s初始化string对象。 |
string(const string& str)(重点) | 拷贝构造函数,用已存在的
|
string(size_t n, char c) | 创建一个包含n个字符c的string对象。 |
#include <iostream> #include <string> using namespace std; int main() { string s1; cout << "s1: " << s1 << endl; string s2("Hello"); // 等价于 const char* cstr = "Hello"; string s2(cstr); cout << "s2: " << s2 << endl; string s3(s2); cout << "s3: " << s3 << endl; string s4(5, 'A'); cout << "s4: " << s4 << endl; return 0; }2.2 容量操作
| 分类 | 函数名称 | 功能说明 | 核心特性与注意事项 |
|---|---|---|---|
| 长度查询 | size()(重点) | 返回字符串有效字符长度(与容器接口统一) | - 与length()底层实现完全一致-优先使用 size(),兼容其他容器 |
length() | 返回字符串有效字符长度(C 风格命名) | - 功能与size()完全相同- 保留用于兼容旧代码习惯 | |
| 容量查询 | capacity() | 返回底层存储空间的总大小(单位:字符数) | - 表示当前已分配的内存空间上限 - 与有效字符数无关 |
| 状态检测 | empty()(重点) | 判断字符串是否为空 | 返回 true 为空,返回 false 非空 |
| 内容清空 | clear()(重点) | 清空有效字符(size()置于0) | 不会释放空间,仅清空内容,不改变底层capacity() |
| 空间管理 | reserve(size_t n)(重点) | 为字符串预留n个字符的存储空间(不改变有效字符数) | - 若 - 仅扩容不缩容,提前预留可避免频繁扩容效率损耗 |
resize(size_t n) | 将有效字符数调整为n,多出空间用\0(ASCII 码 0)填充 | - n > 原长,用 \0 填充;n < 原长,截断 | |
resize(size_t n,char c)(重点) | 将有效字符数调整为n,多出空间用字符 c 填充 | n > 原长,用字符填充;n < 原长,截断。可能改变容量 |
#include <iostream> #include <string> using namespace std; int main() { string str = "Hello"; cout << "字符串 " << str << " 长度: " << str.size() << endl; cout << "字符串 " << str << " 容量: " << str.capacity() << endl; cout << "字符串 " << str << " 是否为空: " << (str.empty() ? "是" : "否") << endl; // clear() string strToClear = "Clear me"; cout << strToClear << "清空前长度: " << strToClear.size() << endl; cout << "清空前容量: " << strToClear.capacity() << endl; strToClear.clear(); cout << "清空后长度: " << strToClear.size() << endl; cout << "清空后容量: " << strToClear.capacity() << endl; // reserve() string strToReserve = "Reserve"; cout << strToReserve << "预留前容量: " << strToReserve.capacity() << endl; cout << "预留前长度: " << strToReserve.size() << endl; strToReserve.reserve(20); cout << "预留 20 字符后容量: " << strToReserve.capacity() << endl; cout << "预留后长度: " << strToReserve.size() << endl; // resize(n) string s1 = "Resize"; cout << s1 << "调整前长度: " << s1.size() << endl; s1.resize(10); cout << "调整为 10 后长度: " << s1.size() << endl; cout << "调整后内容: " << s1 << endl; // resize(n, c) string s2 = "Resize"; cout << s2 << "调整前长度: " << s2.size() << endl; s2.resize(10, '!'); cout << "调整为 10 并用 '!' 填充后长度: " << s2.size() << endl; cout << "调整后内容: " << s2 << endl; return 0; }2.3 访问及遍历
| 函数名称 | 功能说明 |
|---|---|
operator[](重点) |
|
begin() + end() |
|
rbegin() + rend() |
|
| 范围 for 循环(C++ 11) |
|
2.3.1operator[](重点)
#include <iostream> #include <string> using namespace std; int main() { string str = "Hello"; cout << "首字符: " << str[0] << endl; str[1] = 'E'; cout << "修改后字符串: " << str << endl; return 0; }2.3.2begin() + end()
#include <iostream> #include <string> using namespace std; int main() { string str = "World"; for (auto it = str.begin(); it != str.end(); ++it) { cout << *it << " "; } // 迭代器的类型较复杂,auto自动推导类型就无需手动写明类型 cout << endl; return 0; }2.3.3rbegin() + rend()
#include <iostream> #include <string> using namespace std; int main() { string str = "World"; for (auto rit = str.rbegin(); rit != str.rend(); ++rit) { cout << *rit << " "; } cout << endl; return 0; }2.3.4 范围 for
for (声明变量 : 范围) {}#include <iostream> #include <string> using namespace std; int main() { string str = "C++11"; for (char c : str) { cout << c << " "; } cout << endl; return 0; }2.4 修改操作
| 分类 | 函数名称 | 功能说明 | 重点说明 |
|---|---|---|---|
| 尾部追加 | push_back(char c) | 在字符串尾部插入单个字符c。 | 直接操作字符,等价于append(1, c)。 |
append(const string& str) | 在字符串尾部追加另一个字符串str(支持str、const char*、字符序列等多类型)。 | 可指定追加位置和长度,灵活性高。 | |
operator+=(重点) | 在字符串尾部追加字符或字符串,返回当前对象引用。 | 最常用,尾部追加优先用,支持+= 'a'或 += "bcd"。 | |
| 转换 | c_str()(重点) | 返回以'\0'结尾的 C 风格字符串指针(const char*)。 | 用于兼容 C 库函数(如strlen、strcmp),如 printf("%s", s.c_str()) |
| 查找 | find(char c/string s, size_t pos = 0)(重点) | 从索引pos开始向后查找字符c或字符串s,返回首次出现的索引 | 未找到返回string::npos(值为-1) |
rfind(char c/string s, size_t pos = npos) | 从索引pos开始向前查找 | 适用于从后往前搜索的场景(如查找后缀)。 | |
| 截取 | substr(size_t pos = 0, size_t n)(重点) | 从索引pos开始截取长度为n的子字符串并返回 | pos 越界则抛出异常,n 省略则截至末尾 |
int main() { string s = "Hello World"; s.push_back('!'); s += "???"; size_t idx1 = s.find('Wor'); // 从下标0开始查找 size_t idx2 = s.find('!', 3); // 从下标3开始找 size_t idx3 = s.find('z'); // 查找不存在的字符 if (idx3 == string::npos) { cout << "未找到" << endl; } string sub1 = s.substr(2, 3); // 起始下标,截取长度 string sub2 = s.substr(3); // 起始下标,截到末尾 string sub3 = s.substr(); // 截全部 return 0; }2.5 非成员函数(了解)
| 函数名称 | 功能说明 |
|---|---|
operator+ | 拼接字符串,返回新字符串(传值返回,深拷贝可能导致效率问题,建议尽量少用,用+=替代)。 |
operator>>(重点) | 输入字符串(遇空格 / 换行符停止)。 |
operator<<(重点) | 输出字符串(支持连续输出)。 |
getline(cin, str)(重点) | 读取一整行字符串(包含空格,遇换行符停止,换行符不保留)。 |
比较运算符(重点) | 比较字符串大小,按字符字典序(ASCII 码)逐个比较,直到出现差异或到达末尾,支持==、!=、<、>、<=、>= |
2.5.1 >> <<
#include <iostream> #include <string> using namespace std; int main() { string inputStr; cout << "请输入一个字符串(遇空格或换行停止): "; cin >> inputStr; cout << inputStr << endl; string str1 = "Hello"; string str2 = "World"; cout << str1 << " " << str2 << endl; return 0; }2.5.2getline
#include <iostream> #include <string> using namespace std; int main() { string line; cout << "请输入一行字符串(可含空格): "; getline(cin, line); cout << "你输入的是: " << line << endl; cout << "字符串长度: " << line.size() << endl; return 0; }2.5.3 关系运算符
#include <iostream> #include <string> using namespace std; int main() { string str1 = "apple"; string str2 = "banana"; string str3 = "apple"; // == if (str1 == str3) { cout << "str1 等于 str3" << endl; } else { cout << "str1 不等于 str3" << endl; } // 大小比较(按字典序) if (str1 < str2) { cout << "str1(" << str1 << ")小于 str2(" << str2 << ")" << endl; } else if (str1 > str2) { cout << "str1(" << str1 << ")大于 str2(" << str2 << ")" << endl; } else { cout << "str1 等于 str2" << endl; } // != if (str2 != str3) { cout << "str2 不等于 str3" << endl; } return 0; }3. string类的模拟实现(面试常考)
面试常考模拟实现string类,主要是实现string类的构造、拷贝构造、赋值运算符重载及析构函数。
3.1错误示例引入
class String { public: String(const char* s = "") { if (nullptr == s) { assert(false); return; } _s = new char[strlen(s) + 1]; strcpy(_s, s); } ~String() { if (_s) { delete[] _s; _s = nullptr; } } private: char* _s; }; void TestString() { String s1("hello bit!!!"); String s2(s1); }上述String类没有显式定义其拷贝构造函数与赋值运算符重载,编译器会合成默认拷贝构造函数,默认拷贝构造函数是浅拷贝,只复制对象中的值不复制内容。
用s1构造s2时,编译器调用默认拷贝构造,只复制了对象中的char* _s,没有复制该指针指向的堆内存。于是两个对象s1、s2的_s指向同一块内存,析构时这块内存被释放两次,引起程序崩溃。
3.2 浅拷贝危害
浅拷贝(值拷贝)指编译器仅复制对象中的值。
若对象涉及资源管理,会导致多个对象共享同一资源。当某一对象销毁并释放资源后,其他对象仍认为资源有效,继续操作该资源时将引发访问违规。
采用深拷贝可解决以上问题,即每个对象拥有独立的内存空间,避免共享资源。
3.3 深拷贝
如果一个类中涉及资源管理,其拷贝构造函数、赋值运算符重载及析构函数必须显式定义,从而实现深拷贝。
现代版深拷贝模拟实现String类(面试必问)
#include <iostream> #include <cassert> #include <cstring> // strlen、strcpy using namespace std; class String { public: String(const char* str = "") // 默认值为空字符串 { if (nullptr == str) { assert(false); // 直接终止程序并提示错误位置 return; } _str = new char[strlen(str) + 1]; // strlen(str)计算字符串str有效字符个数 // 多分配 1 字节空间用于存 \0 strcpy(_str, str); // 将源字符串str完整(含\0)拷贝到_str指向的空间 } // 拷贝构造 String(const String& s) { String temp = s._str; // 用 s._str 创建并初始化临时对象temp // 若此时_str未初始化那它就是野指针,swap后析构 delete[] _str 就是释放野指针使程序崩溃,而 delete[] nullptr 在C++是安全 swap(_str, temp._str); // 交换当前对象和temp的字符串 _str=nullptr; // 避免swap后临时对象持有野指针 } // 赋值运算符重载 String& operator=(String s) // 传值调用触发拷贝构造,生成一个s的副本(独立内存) { swap(_str, s._str); // 交换前s._str指向副本内存;交换后s._str指向原内存——析构时释放原内存 return *this; } ~String() { if (_str) // 判断_str非nullptr而是有效指针——虽然 delete[] nullptr 在C++中是安全的,但这样增强代码可读性 { delete[] _str; _str = nullptr; // 释放资源后置空避免野指针 } } private: char* _str; };| 传统版模拟实现String类 | 现代版 | 现代版优势 | |
| 拷贝构造 | String(const String& s) | String(const String& s) |
|
| 赋值运算符重载(重点对比) | String& operator=(const String& s) // 要素③:释放旧资源 | String& operator=(String s) { swap(_str, s._str); return *this; } |
4. 笔试高频OJ题(必掌握)
以下题目均为字符串模块高频笔试真题,代码可直接复用,需理解核心思路。
4.1 仅仅反转字母
给定一个字符串如 a-bC-dEf-ghIj ,仅反转字符串中字母的位置,非字母的字符位置不变。
核心思路:首尾向中间靠拢,跳过非字母字符后交换。
#include <iostream> #include <string> using namespace std; class Solution { public: bool isLetter(char c) { if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { return true; } return false; } string reverseOnLetter(string s) { if (s.empty()) return s; // 字符串判空返回,避免无效操作及潜在问题 size_t begin = 0, end = s.size() - 1; // size_t取值非负,适合存储索引 while (begin < end) // 左右指针未相遇,即还有未处理的字符 { // “左指针”跳过非字母 while (!isLetter(s[begin])) ++begin; // “右指针”跳过非字母 while (!isLetter(s[end])) --end; swap(s[begin], s[end]); ++begin; --end; } return s; } }; int main() { string str = "a-bC-dEf-ghIj"; Solution sol; cout << "原字符串:" << str << endl; cout << "反转字母后的字符串:" << sol.reverseOnLetter(str) << endl; return 0; }4.2 第一个只出现一次的字符
给定一个字符串,若能找到第一个只出现一次的字符,返回其索引;若不存在,则返回 -1 。
核心思路:用数组统计每个ASCII字符的出现字数。
#include <iostream> #include <string> using namespace std; class Solution {public: int firstUniqChar(string s) { int count[256] = { 0 }; // 计算机中字符本质是用ASCII码表示的整数,而扩展ASCII码共256个可能取值 // 所有字符的次数初始化为 0 for (int i = 0; i < s.size(); ++i) { count[s[i]]++; // C++中字符可隐式转换为对应ASCII码 } for (int i = 0; i < s.size(); ++i) { if (count[s[i]] == 1) return i; } return -1; } }; int main() { string str = "leetcode"; Solution sol; cout << "原字符串:" << str << endl; cout << "第一个只出现一次的字符索引:" << sol.firstUniqChar(str) << endl; return 0; }4.3 最后一个单词的长度
给定一个由若干单词组成的句子,单词由大小写字母混合构成,单词间用单个空格分隔,求最后一个单词的长度。
核心思路:从末尾反向查找最后一个空格,计算长度。
#include <iostream> #include <string> using namespace std; int main() { string s = "You Are Beautiful"; while (getline(cin, s)) // 读取整行字符串(含空格) { size_t pos = s.rfind(' '); // 最后一个空格的索引 cout << s.size() - 1 - pos << endl; } return 0; }4.4 验证一个字符串是否为回文串
给定一个字符串如 A man, a plan, a canal: Panama ,如果它是回文串,返回true;否则,返回false。
回文串:不区分大小写(所有大写字符转为小写字符或所有小写字符转为大写字符),忽略非字母数字字符(如标点符号、空格),正读和反读相同的字符串。
核心思路:双指针过滤非字母数字字符,并统一大小写后比较。
#include <iostream> #include <string> using namespace std; class Solution { public: bool isLetterOrNumber(char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } bool isPalindrome(string s) { // 先统一大小写 for (auto& c : s) { if (c >= 'A' && c <= 'Z') { c += 32; // 小写ASCII码 = 大写ASCII码 + 32 } } int begin = 0, end = s.size() - 1; while (begin < end) { while (!isLetterOrNumber(s[begin])) ++begin; while (!isLetterOrNumber(s[end])) --end; if (s[begin] != s[end]) // 左右不等 return false; // 左右相等 ++begin; --end; } return true; } }; int main() { string str = "A man,a plan,a canal:Panama"; Solution sol; cout << str <<(sol.isPalindrome(str)?" 是":" 不是") <<"回文串" << endl; return 0; }4.5 字符串相加
给定两个非负整数字符串 n1(如"123")和 n2(如"456"),计算它们的和并同样以字符串形式返回。不能使用任何內建的用于处理大整数的库, 也不能直接将输入的字符串转换为整数形式。
核心思路:从“个位”开始逐位相加,处理进位,最后反转结果。
#include <iostream> #include <string> using namespace std; class Solution { public: string addStrings(string n1, string n2) { int end1 = n1.size() - 1; // 最后一个字符(个位)索引 int end2 = n2.size() - 1; int value1 = 0, value2 = 0, carry = 0; string result; while (end1 >= 0 || end2 >= 0) // 未遍历完 { // 字符'0'~'9'的ASCII码连续,如'3'-'0'=3,即字符转为数字 // 若end1<0,即遍历完,那该位取0 value1 = (end1 >= 0) ? (n1[end1--] - '0') : 0; value2 = (end2 >= 0) ? (n2[end2--] - '0') : 0; int sum = value1 + value2 + carry; // 两个数字位 + 更低一位的进位 carry = sum / 10; // 当前位的进位,如5+7=12,进位就是1 result += (sum % 10) + '0'; // 当前位结果去进位后转字符,加进结果 } // 此时逐位相加的循环已止,carry表示最高位的进位 if (carry == 1) { result += '1'; } // 从右向左逐位相加,但result字符串是从左向右加字符 reverse(result.begin(), result.end()); return result; } }; int main() { Solution sol; string num1 = "123"; string num2 = "456"; string sum = sol.addStrings(num1, num2); cout << "非负整数字符串之和: " << sum << endl; return 0; }