【C++】string 类从入门到深入:构造、常用接口、OJ 题与模拟实现
不会 string,等于不会 C++ 字符串
从 C 风格字符串到 C++ string,从使用到手写模拟
一、为什么需要 string 类?
C 语言中的字符串问题
- 以
\0结尾的字符数组 - 操作函数(strcpy/strcat/strlen)与数据分离,不符合 OOP 思想
- 用户需手动管理内存,极易越界、泄漏
C++ string 的优势
- 面向对象:字符串与操作封装在一起
- 自动内存管理
- 支持赋值、比较、拼接、查找、遍历等丰富接口
- 安全性、便利性远超 C 字符串
工作中几乎不再直接使用 C 字符串函数
二、C++11 小语法铺垫:auto 与范围 for
1. auto 类型推导
C++11 赋予 auto 新含义:编译器自动推导类型
cpp
auto a = 10; // int
auto b = 'a'; // char
auto c = func(); // 根据返回值推导
auto& ref = a; // 必须加 &
auto* p = &a; // auto 和 auto* 等价
注意
- 同一行多变量类型必须一致
- 不能作为参数(可作为返回值,但慎用)
- 不能直接声明数组
典型用法:简化迭代器
cpp
map<string, string>::iterator it = dict.begin();
auto it = dict.begin(); // 简洁
2. 范围 for(Range-based for)
自动遍历数组或容器:
cpp
int arr[] = {1,2,3};
for(auto& e : arr) e *= 2; // 修改原值
for(auto e : arr) cout << e << " ";
string str = "hello";
for(char ch : str) cout << ch << " ";
底层本质:替换为迭代器
三、string 常用接口精讲
以下为最常用接口,掌握即可应对 90% 场景
1. 构造
| 构造方式 | 示例 |
|---|---|
| 空字符串 | string s1; |
| 用 C 字符串 | string s2("hello"); |
| 拷贝构造 | string s3(s2); |
| 填充构造 | string s4(5, 'A'); |
2. 容量
| 方法 | 含义 |
|---|---|
size() / length() |
有效字符长度 |
capacity() |
当前底层容量 |
empty() |
是否为空 |
clear() |
清空,不释放内存 |
reserve(n) |
预留空间(不改变 size) |
resize(n, c) |
改变有效字符数 |
示例
cpp
string s("hello");
cout << s.size(); // 5
s.reserve(100); // 容量 ≥100
s.resize(10, '!'); // hello!!!!!
3. 访问与遍历
| 方式 | 示例 |
|---|---|
operator[] |
s[0] |
| 迭代器 | begin() / end() |
| 范围 for | for(char c : s) |
4. 修改操作
| 方法 | 作用 |
|---|---|
push_back(c) |
尾插字符 |
append(str) |
追加字符串 |
operator+= |
最常用 |
c_str() |
返回 C 字符串 |
find(c, pos) |
正向查找 |
rfind(c, pos) |
反向查找 |
substr(pos, n) |
截取子串 |
推荐使用 +=
cpp
string s = "hello";
s += " world";
s += '!';
5. 非成员函数
getline(cin, s):读取一行(含空格)operator>>/operator<<- 关系运算符:
==、<、>等
四、VS 与 g++ 下的 string 底层结构(面试常问)
VS(32位):28 字节
- 字符串 ≤ 15:存放在内部数组合法
- 字符串 ≥ 16:堆上分配
- 结构:联合体 _Bxty + size + capacity + 额外指针
小字符串优化(SSO)
g++(32位):4 字节
- 只存一个指针,指向堆区结构
- 堆区包含:_M_length、_M_capacity、_M_refcount(引用计数)
- 采用 写时拷贝(COW)
五、牛刀小试:经典 OJ 题(附代码)
1. 仅反转字母
保留非字母位置,反转字母部分
cpp
class Solution {
public:
bool isLetter(char ch) {
return (ch>='a'&&ch<='z') || (ch>='A'&&ch<='Z');
}
string reverseOnlyLetters(string S) {
int l=0, r=S.size()-1;
while(l<r){
while(l<r && !isLetter(S[l])) ++l;
while(l<r && !isLetter(S[r])) --r;
swap(S[l++], S[r--]);
}
return S;
}
};
2. 字符串中第一个只出现一次的字符(哈希思想)
cpp
class Solution {
public:
int firstUniqChar(string s) {
int count[256] = {0};
for(char ch : s) count[ch]++;
for(int i=0; i<s.size(); ++i)
if(count[s[i]] == 1) return i;
return -1;
}
};
3. 验证回文串(忽略非字母数字 + 大小写)
cpp
class Solution {
public:
bool isPalindrome(string s) {
for(char& ch : s)
if(ch>='A' && ch<='Z') ch += 32;
int l=0, r=s.size()-1;
while(l<r){
while(l<r && !isalnum(s[l])) ++l;
while(l<r && !isalnum(s[r])) --r;
if(s[l]!=s[r]) return false;
++l; --r;
}
return true;
}
};
4. 字符串相加(大数加法)
cpp
string addStrings(string num1, string num2) {
int i=num1.size()-1, j=num2.size()-1, carry=0;
string res;
while(i>=0 || j>=0 || carry){
int sum = carry;
if(i>=0) sum += num1[i--]-'0';
if(j>=0) sum += num2[j--]-'0';
carry = sum/10;
res += (sum%10)+'0';
}
reverse(res.begin(), res.end());
return res;
}
六、string 模拟实现(面试必考)
1. 浅拷贝的问题(经典错误)
cpp
class String {
char* _str;
};
String s1("hello");
String s2(s1); // 默认拷贝 → 两个对象指向同一块内存 → 析构两次 → 崩溃
2. 深拷贝(传统版)
显式实现拷贝构造、赋值、析构
cpp
String(const String& s) {
_str = new char[strlen(s._str)+1];
strcpy(_str, s._str);
}
String& operator=(const String& s) {
if(this != &s) {
delete[] _str;
_str = new char[strlen(s._str)+1];
strcpy(_str, s._str);
}
return *this;
}
3. 深拷贝(现代版 --- 更简洁)
cpp
String(const String& s) : _str(nullptr) {
String tmp(s._str);
swap(_str, tmp._str);
}
String& operator=(String s) { // 传值,直接复用拷贝构造
swap(_str, s._str);
return *this;
}
4. 写时拷贝(了解)
- 引用计数 + 浅拷贝
- 只有"写"时才真正复制
- g++ 早期采用,现代已较少使用
七、总结
| 知识点 | 核心要点 |
|---|---|
auto |
类型推导,简化迭代器 |
| 范围 for | 容器/数组自动遍历 |
| string 接口 | size, resize, reserve, +=, find, substr |
| OJ 常见题 | 回文、首个唯一字符、大数加法 |
| 深拷贝 | 解决浅拷贝资源重复释放 |
| 现代版写法 | 传值 + swap,简洁优雅 |
建议:把上面的 OJ 题手写 2~3 遍,string 模拟实现至少写一遍传统版 + 现代版。
📌 延伸阅读
- 面试中 string 的一种正确写法
- STL 中的 string 类到底怎么了?
如有疑问或想深入某一部分,欢迎留言讨论。