1. 为什么学习 string 类?
在C语言中,字符串是以 \0 结尾的字符数组。虽然C标准库提供了 strlen、strcpy、strcat 等一系列强大的库函数,但在实际开发中,它们存在明显的局限性:
-
不符合面向对象思想:数据和操作数据的方法是分离的(函数操作字符串)。
-
安全性低 :底层空间需要程序员手动管理(
malloc/free),稍有不慎就会导致内存泄漏或缓冲区溢出(越界访问)。 -
操作繁琐:进行字符串拼接、查找、赋值等操作时,代码逻辑往往比较繁琐。
为了解决这些问题,C++ 标准库提供了 string 类。它封装了字符串的底层存储,自动管理内存,并提供了丰富的成员函数。
2. 标准库中的 string 类
2.1 string 类简介
string 是C++标准模板库(STL)中的一个类模板 basic_string 的实例化。
-
本质 :
typedef basic_string<char, char_traits, allocator> string; -
头文件 :
#include <string> -
命名空间 :
using namespace std;
注意 :string 类独立于编码处理字节。如果处理多字节字符(如UTF-8)size() 或 length() 返回的是字节数,而不是实际字符数(这一点在多语言环境下需格外留意)。
2.2 string 类的常用接口详解
2.2.1 常见构造
| 构造函数 | 功能说明 |
|---|---|
string() |
构造空的string对象(空字符串) |
string(const char* s) |
用C风格字符串构造 |
string(size_t n, char c) |
构造包含n个字符c的对象 |
string(const string& s) |
拷贝构造函数 |
代码演示:
cpp
#include <iostream>
#include <string>
using namespace std;
void TestStringConstruct()
{
string s1; // 空字符串
string s2("Hello C++"); // 使用C字符串初始化
string s3(5, 'A'); // "AAAAA"
string s4(s2); // 拷贝构造,s4也是"Hello C++"
cout << "s2: " << s2 << endl;
cout << "s3: " << s3 << endl;
cout << "s4: " << s4 << endl;
}
2.2.2 容量操作
| 函数名称 | 功能说明 |
|---|---|
size() / length() |
返回字符串有效字符长度(重点:不含 \0) |
capacity() |
返回当前分配的空间总大小(即无需重新分配内存就能容纳的字符数) |
empty() |
判断是否为空串 |
clear() |
清空有效字符,但不释放底层内存 |
reserve() |
预留空间(改变 capacity,不改变 size) |
resize() |
改变有效字符个数(改变 size)。若增加,多出的空间用指定字符填充;若减少,截断字符串。 |
代码演示(容量与预留空间):
cpp
#include<iostream>
#include<string>
using namespace std;
void TestCapacity()
{
string s("Hello");
cout << "size: " << s.size() << " capacity: " << s.capacity() << endl; // 输出: size:5 capacity:15(可能因编译器而异)
// 1. reserve: 预留空间,避免频繁扩容
s.reserve(100);
cout << "After reserve(100), capacity: " << s.capacity() << endl;
// 2. resize: 改变长度
s.resize(10, 'X'); // 将字符串长度扩展到10,多出的位置用'X'填充
cout << "After resize(10, 'X'): " << s << endl; // HelloXXXXX
s.resize(3);// 截断为3个字符
cout << "After resize(3): " << s << endl; // Hel
// 3. clear: 清空内容
s.clear();
cout << "After clear, size: " << s.size() << ", empty: " << s.empty() << endl; // size:0, empty:1
}
2.2.3 访问与遍历
| 函数名称 | 功能说明 |
|---|---|
operator[] |
返回 pos 位置的字符(推荐,支持随机访问) |
begin() + end() |
正向迭代器 |
rbegin() + rend() |
反向迭代器 |
| 范围for | C++11语法糖,简洁高效 |
代码演示(三种遍历方式):
cpp
void TestTraverse()
{
string s("Hello World");
// 1. 下标 + operator[]
for (size_t i = 0; i < s.size(); ++i)
{
cout << s[i] << " ";
}
cout << endl;
// 2. 迭代器 (类似指针)
//auto it = s.begin(); 可以使用auto
for (string::iterator it = s.begin(); it != s.end(); ++it)
{
cout << *it << " ";
}
cout << endl;
// 3. 范围for (C++11)
for (char ch : s)
{
cout << ch << " ";
}
cout << endl;
}
2.2.4 修改操作
| 函数名称 | 功能说明 |
|---|---|
push_back(char c) |
尾插一个字符 |
append(const string& str) |
追加字符串 |
operator+= |
最常用,追加字符串或字符 |
c_str() |
返回C风格的字符串(const char*) |
find() / rfind() |
查找字符或子串,返回索引,未找到返回 npos |
substr() |
截取子串 |
代码演示(追加、查找与截取):
cpp
void TestModify()
{
string s = "Hello";
// 1. 追加
s.push_back(' ');
s.append("C++");
s += " 2026";
cout << s << endl; // Hello C++ 2026
// 2. 查找
string url = "https://www.example.com";
size_t pos = url.find("www");
if (pos != string::npos)
{
cout << "'www' found at index: " << pos << endl; // 8
}
// 3. 截取子串 (从pos开始,截取n个字符)
string domain = url.substr(pos, 3);
cout << "Domain: " << domain << endl; // www
// 4. C风格输出
const char* cstr = s.c_str();
printf("C style: %s\n", cstr);
}
2.2.5 非成员函数
| 函数 | 功能说明 |
|---|---|
operator+ |
字符串拼接(效率较低,可能深拷贝,建议少用) |
operator>> |
流提取(输入),以空格为分隔符 |
operator<< |
流插入(输出) |
getline() |
获取一行字符串(包含空格) |
relational operators |
支持 >、<、== 等比较操作 |
注意 :当需要读取包含空格的字符串时,必须使用 getline 而不是 cin >>。
2.3 底层结构探秘(vs 与 g++)
了解底层结构有助于我们理解性能开销:
-
VS (Visual Studio) 下的 string :
采用 小字符串优化 (SSO) 。
string对象占用 28 字节(32位)。-
如果字符串长度小于 16,存储在对象内部的固定数组中,不进行堆分配。
-
如果长度大于等于 16,则在堆上开辟空间。
这种设计极大提升了短字符串的处理效率。
-
-
G++ 下的 string :
采用 写时拷贝 (COW) 策略(早期版本,现代版本已变化)。
string对象只占用 4 字节(32位),内部包含一个指向堆空间的指针。堆空间除了存储字符串外,还存储了长度、容量和引用计数,用于管理内存共享。
3. 经典题目实战(牛刀小试)
理论结合实践,下面我们通过几道经典OJ题目,来感受 string 的威力。
3.1 题目一:仅仅反转字母
题目链接 :LeetCode 917. 仅仅反转字母
题目描述 :
给定一个字符串 S,返回 "反转后的" 字符串,其中不是字母的字符都保留在原地,而所有字母的位置发生反转。
完整代码:
cpp
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;
int begin = 0;
int end = S.size() - 1;
while (begin < end)
{
// 跳过非字母
while (!isLetter(S[begin]) && begin < end) begin++;
while (!isLetter(S[end]) && begin < end) end--;
// 交换字母
swap(S[begin++], S[end--]);
}
return S;
}
};
3.2 题目二:找字符串中第一个只出现一次的字符
题目链接 :LeetCode 387. 字符串中的第一个唯一字符
题目描述 :
给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。
思路解析:
-
利用哈希表(数组模拟)统计每个字符出现的次数。由于ASCII字符范围是0-255,可以开辟一个大小为256的数组。
-
第一次遍历:统计
count[s[i]]++。 -
第二次遍历:从前往后扫描字符串,如果某个字符的计数为1,则直接返回其下标。
-
遍历结束未找到,返回 -1。
完整代码:
cpp
class Solution
{
public:
int firstUniqChar(string s)
{
// 1. 统计频率
int count[256] = { 0 }; // 初始化为0
for (char ch : s)
{
count[ch]++;
}
// 2. 查找第一个频率为1的字符
for (int i = 0; i < s.size(); ++i)
{
if (count[s[i]] == 1)
{
return i;
}
}
return -1;
}
};
3.3 题目三:验证回文串
题目链接 :LeetCode 125. 验证回文串
题目描述 :
如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个回文串。
思路解析:
-
预处理:遍历字符串,将大写字母转为小写(或统一转为大写)。这一步也可以不提前做,在比较时动态转换。
-
双指针 :定义
begin和end。 -
跳过非字母数字 :如果
begin指向的字符不是字母或数字,begin++;如果end指向的不是,end--。 -
比较 :比较
s[begin]和s[end](忽略大小写),如果不相等返回false。相等则移动指针继续。
完整代码:
cpp
class Solution
{
public:
bool isSmallLetter(char ch)
{
return (ch >= 'a' && ch <= 'z');
}
bool isPalindrome(string s)
{
// 1. 统一转成小写,方便比较
for (char& ch : s)
{
if (ch >= 'A' && ch <= 'Z')
{
ch += 32; // 转小写
}
}
int begin = 0;
int end = s.size() - 1;
while (begin < end)
{
// 跳过小写字母
while (begin < end && !isSmallLetter(s[begin])) begin++;
while (begin < end && !isSmallLetter(s[end])) end--;
// 比较
if (s[begin++] != s[end--])
{
return false;
}
}
return true;
}
};
3.4 题目四:字符串相加
题目链接 :LeetCode 415. 字符串相加
题目描述 :
给定两个字符串形式的非负整数 num1 和 num2,计算它们的和并同样以字符串形式返回。不能使用任何内建的用于处理大整数的库(比如 BigInteger),也不能直接将输入的字符串转换为整数形式。
思路解析 :
模拟竖式加法。
-
定义两个指针
end1和end2分别指向两个字符串的末尾。 -
定义进位
next初始为0。 -
循环条件:
end1 >= 0或end2 >= 0或next != 0。-
取出当前位的数字(如果指针越界,则取0)。
-
计算和:
val = value1 + value2 + next。 -
更新进位:
next = val / 10;当前位:cur = val % 10。 -
将
cur转换为字符插入到结果字符串中。
-
-
因为我们是按从低位到高位计算的,最后需要将结果字符串反转(或者使用头插法,但头插效率低,推荐尾插后反转)。
完整代码:
cpp
class Solution
{
public:
string addStrings(string num1, string num2)
{
int end1 = num1.size() - 1;
int end2 = num2.size() - 1;
int next = 0; // 进位
string result;
while (end1 >= 0 || end2 >= 0 || next != 0)
{
// 获取当前位的数字
int val1 = (end1 >= 0) ? num1[end1--] - '0' : 0;
int val2 = (end2 >= 0) ? num2[end2--] - '0' : 0;
int sum = val1 + val2 + next;
next = sum / 10;
int cur = sum % 10;
// 尾插当前位
result += (cur + '0');
}
// 因为我们是从个位开始算的,结果顺序是反的,需要反转
reverse(result.begin(), result.end());
return result;
}
};
4. 课后作业(挑战升级)
下面几道题留给大家作为练习,进一步巩固 string 的使用:
-
翻转字符串 II :给定一个字符串
s和一个整数k,从开头开始,每隔2k个字符,反转前k个字符。如果剩余字符少于k个,则将剩余字符全部反转;如果剩余字符小于2k但大于等于k个,则反转前k个字符。 (LeetCode 541) -
翻转字符串 III:给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。 (LeetCode 557)
-
字符串相乘 :给定两个以字符串形式表示的非负整数
num1和num2,返回num1和num2的乘积,它们的乘积也表示为字符串形式。不能使用内置库处理大整数。 (LeetCode 43) -
找出字符串中第一个只出现一次的字符(进阶版):如果字符串很长,且字符集很大(不仅仅是字母),如何优化?
5. 总结
string 类是C++中最常用、最重要的类之一。
-
核心要点:
-
使用
size()获取长度,capacity()获取容量。 -
利用
reserve()预分配空间,减少扩容开销。 -
+=操作符是追加字符串最优雅的方式。 -
通过
c_str()获取底层C字符串以便与旧代码交互。 -
find与substr是处理子串问题的利器。
-