C++ string 类全面解析
1. 为什么学习 string 类?
1.1 C语言中的字符串局限性
在C语言中,字符串是以\0
结尾的字符数组,这种表示方式存在几个明显的缺陷:
C语言字符串的主要问题:
- 安全性问题:容易发生缓冲区溢出,导致程序崩溃或安全漏洞
- 内存管理复杂:需要手动管理内存分配和释放,容易造成内存泄漏
- 功能有限:标准库函数功能相对基础,复杂的字符串操作需要自行实现
- 不符合面向对象思想:数据与操作分离,不符合现代编程范式
cpp
// C语言字符串操作的典型问题
char str[10];
strcpy(str, "这个字符串太长了会导致溢出"); // 潜在的安全风险
1.2 实际应用需求
在现代编程中,字符串处理占据了极大的比重。无论是Web开发、数据处理还是系统编程,都离不开高效的字符串操作。string类的出现正是为了解决C语言字符串的种种痛点。
面试题示例(后续详解):
- 字符串转整型数字
- 大数相加(字符串形式)
实践建议:在OJ题目和实际开发中,string类已成为字符串处理的首选工具,相比C字符串库函数更加安全高效。
2. 标准库中的string类
2.1 string类基础
string类是C++标准库中用于表示和操作字符串的类,封装了字符串的存储和常见操作。
基本用法:
cpp
#include <string>
#include <iostream>
using namespace std;
int main() {
string s1; // 空字符串
string s2 = "Hello"; // 直接初始化
string s3("World"); // 构造函数初始化
return 0;
}
2.2 C++11新特性:auto和范围for
auto关键字详解
auto是C++11引入的类型推断关键字,让编译器自动推导变量类型。
auto的使用规则:
cpp
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
// 基本类型推断
auto a = 10; // int
auto b = 3.14; // double
auto c = 'A'; // char
// 指针和引用
int x = 100;
auto y = &x; // int*
auto* z = &x; // int* (与上面等价)
auto& ref = x; // int&
// 容器迭代器简化
map<string, string> dict = {{"apple", "苹果"}, {"banana", "香蕉"}};
// 传统写法(冗长)
map<string, string>::iterator it1 = dict.begin();
// auto写法(简洁)
auto it2 = dict.begin();
// 遍历map
for (auto it = dict.begin(); it != dict.end(); ++it) {
cout << it->first << ": " << it->second << endl;
}
return 0;
}
auto的限制:
- 必须初始化:
auto x;
// 错误 - 多变量声明必须类型一致:
auto a=1, b=2.0;
// 错误 - 不能用于函数参数(但可以用于返回值)
- 不能声明数组:
auto arr[] = {1,2,3};
// 错误
范围for循环(Range-based for loop)
范围for提供了更简洁的遍历语法,特别适合容器遍历。
cpp
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main() {
// 数组遍历
int arr[] = {1, 2, 3, 4, 5};
// 传统遍历方式
for (int i = 0; i < sizeof(arr)/sizeof(arr[0]); ++i) {
cout << arr[i] << " ";
}
cout << endl;
// 范围for遍历(只读)
for (auto elem : arr) {
cout << elem << " ";
}
cout << endl;
// 范围for遍历(可修改)
for (auto& elem : arr) {
elem *= 2; // 修改元素
}
// 字符串遍历
string str = "Hello";
for (auto ch : str) {
cout << ch << " ";
}
cout << endl;
return 0;
}
范围for的底层原理:编译器会将范围for转换为基于迭代器的普通循环。
2.3 string类常用接口详解
2.3.1 构造函数
构造函数 | 功能说明 | 示例 |
---|---|---|
string() |
创建空字符串 | string s1; |
string(const char* s) |
用C字符串构造 | string s2("hello"); |
string(size_t n, char c) |
n个字符c组成的字符串 | string s3(5, 'A'); // "AAAAA" |
string(const string& str) |
拷贝构造 | string s4(s2); |
cpp
void testConstructors() {
string s1; // 空字符串
string s2("Hello World"); // 从C字符串构造
string s3(s2); // 拷贝构造
string s4(10, '*'); // "**********"
string s5 = "直接赋值"; // 赋值初始化
cout << "s2: " << s2 << endl;
cout << "s4: " << s4 << endl;
}
2.3.2 容量操作
重要容量方法:
方法 | 功能 | 说明 |
---|---|---|
size() /length() |
返回字符串长度 | 两者功能相同,推荐size() |
capacity() |
返回分配的内存大小 | 通常 ≥ size() |
empty() |
判断是否为空 | 空返回true,否则false |
clear() |
清空内容 | 不释放内存,size=0 |
reserve(size_t n) |
预留空间 | 避免频繁重新分配 |
resize(size_t n, char c) |
调整大小 | 多出部分用c填充 |
cpp
void testCapacity() {
string str = "Hello";
cout << "长度: " << str.size() << endl; // 5
cout << "容量: " << str.capacity() << endl; // 15(编译器相关)
cout << "是否为空: " << str.empty() << endl; // 0(false)
str.resize(10, '!');
cout << "调整后: " << str << endl; // "Hello!!!!!"
str.reserve(100);
cout << "预留后容量: " << str.capacity() << endl;
str.clear();
cout << "清空后长度: " << str.size() << endl; // 0
}
resize()详解:
cpp
string s = "Hello";
s.resize(3); // "Hel"(截断)
s.resize(8, '!'); // "Hel!!!!!"(扩展并填充)
s.resize(10); // "Hel!!!!! "(扩展,默认填充空格)
2.3.3 元素访问和遍历
多种遍历方式:
cpp
void testTraversal() {
string str = "ABCDE";
// 1. 下标操作符[]
for (size_t i = 0; i < str.size(); ++i) {
cout << str[i] << " "; // A B C D E
}
cout << endl;
// 2. 迭代器
for (auto it = str.begin(); it != str.end(); ++it) {
cout << *it << " "; // A B C D E
}
cout << endl;
// 3. 反向迭代器
for (auto rit = str.rbegin(); rit != str.rend(); ++rit) {
cout << *rit << " "; // E D C B A
}
cout << endl;
// 4. 范围for(推荐)
for (auto ch : str) {
cout << ch << " "; // A B C D E
}
cout << endl;
}
2.3.4 修改操作
常用修改方法:
方法 | 功能 | 示例 |
---|---|---|
push_back(char c) |
尾部添加字符 | str.push_back('!') |
append(const string& str) |
追加字符串 | str.append(" World") |
operator+= |
追加(最常用) | str += "!!" |
insert(size_t pos, const string& str) |
插入字符串 | str.insert(5, "插入") |
erase(size_t pos, size_t len) |
删除子串 | str.erase(5, 2) |
replace(size_t pos, size_t len, const string& str) |
替换子串 | str.replace(0, 5, "Hi") |
cpp
void testModification() {
string str = "Hello";
// 追加操作
str.push_back('!'); // "Hello!"
str.append(" World"); // "Hello! World"
str += "!!"; // "Hello! World!!"(最常用)
// 插入和删除
str.insert(6, "C++ "); // "Hello! C++ World!!"
str.erase(0, 7); // "C++ World!!"
str.replace(4, 5, "String"); // "C++ String!!"
cout << "最终结果: " << str << endl;
}
2.3.5 字符串操作
查找和子串操作:
cpp
void testStringOperations() {
string str = "Hello World, Hello C++";
// 查找操作
size_t pos1 = str.find("Hello"); // 0
size_t pos2 = str.find("Hello", 1); // 13(从位置1开始找)
size_t pos3 = str.rfind("Hello"); // 13(从后往前找)
// 子串提取
string sub1 = str.substr(6, 5); // "World"
string sub2 = str.substr(6); // "World, Hello C++"
// 比较操作
string s1 = "apple", s2 = "banana";
int result = s1.compare(s2); // 负数(apple < banana)
cout << "find结果: " << pos1 << ", " << pos2 << ", " << pos3 << endl;
cout << "子串: " << sub1 << ", " << sub2 << endl;
}
2.4 不同编译器下的string实现
VS下的string实现(小字符串优化)
Visual Studio采用小字符串优化(SSO)策略:
内存布局(32位平台):
- 16字节缓冲区:长度<16时使用栈空间
- 4字节:字符串长度
- 4字节:总容量
- 4字节:其他信息
- 总计28字节
优势:短字符串无需堆分配,提高性能。
cpp
// VS中小字符串优化的效果
string shortStr = "short"; // 使用内部缓冲区(栈)
string longStr = "这是一个很长的字符串..."; // 使用堆分配
g++下的string实现(写时拷贝)
g++采用写时拷贝(Copy-On-Write)技术:
内存布局:
- 4字节指针:指向堆上的结构体
- 结构体包含:长度、容量、引用计数、字符串数据
优势:拷贝时不立即复制数据,提高拷贝效率。
cpp
// g++中的写时拷贝
string s1 = "hello";
string s2 = s1; // 不复制数据,只增加引用计数
s2[0] = 'H'; // 此时才真正复制数据(写时拷贝)
2.5 实战练习
示例1:仅反转字母
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 left = 0, right = s.size() - 1;
while (left < right) {
// 找到左边的字母
while (left < right && !isLetter(s[left])) ++left;
// 找到右边的字母
while (left < right && !isLetter(s[right])) --right;
// 交换字母
if (left < right) {
swap(s[left], s[right]);
++left;
--right;
}
}
return s;
}
};
示例2:字符串相加(大数加法)
cpp
class Solution {
public:
string addStrings(string num1, string num2) {
int i = num1.size() - 1, j = num2.size() - 1;
int carry = 0;
string result;
while (i >= 0 || j >= 0 || carry > 0) {
int digit1 = (i >= 0) ? num1[i--] - '0' : 0;
int digit2 = (j >= 0) ? num2[j--] - '0' : 0;
int sum = digit1 + digit2 + carry;
carry = sum / 10;
result.push_back('0' + (sum % 10));
}
reverse(result.begin(), result.end());
return result;
}
};
示例三:字符串最后一个单词的长度

cpp
#include <iostream>
using namespace std;
int main() {
string s;
getline(cin,s);//这里不能用流提取。流提取识别不了空格
size_t pos=s.rfind(' ');
cout<<s.size()-(pos+1)<<endl;
}

getlin默认三个参数,输入两个时,默认最后一个是空
3. string类的模拟实现
3.1 浅拷贝问题
浅拷贝的危险性:
cpp
// 有问题的String类实现
class String {
public:
String(const char* str = "") {
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String() {
delete[] _str;
}
private:
char* _str;
};
void testProblem() {
String s1("hello");
String s2(s1); // 浅拷贝:s1和s2指向同一内存
// 析构时:同一内存被删除两次 → 程序崩溃
}
3.2 深拷贝实现
传统版String类
cpp
class String {
public:
// 构造函数
String(const char* str = "") {
if (str == nullptr) str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
// 拷贝构造函数(深拷贝)
String(const String& other) {
_str = new char[strlen(other._str) + 1];
strcpy(_str, other._str);
}
// 赋值运算符重载
String& operator=(const String& other) {
if (this != &other) { // 防止自赋值
char* temp = new char[strlen(other._str) + 1];
strcpy(temp, other._str);
delete[] _str; // 释放原有资源
_str = temp;
}
return *this;
}
// 析构函数
~String() {
delete[] _str;
}
private:
char* _str;
};
现代版String类(更优雅)
cpp
class String {
public:
String(const char* str = "") {
if (str == nullptr) str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
// 拷贝构造函数:利用临时对象交换
String(const String& other) : _str(nullptr) {
String temp(other._str); // 用C字符串构造临时对象
swap(_str, temp._str); // 交换资源
}
// 赋值运算符:参数为值传递,利用交换技术
String& operator=(String other) { // 注意:参数为值传递
swap(_str, other._str); // 交换资源
return *this; // other析构时会释放原有资源
}
~String() {
delete[] _str;
}
private:
char* _str;
};
3.3 写时拷贝(Copy-On-Write)
写时拷贝是一种优化技术,在读取时共享数据,在修改时才进行实际拷贝。
基本原理:
- 多个对象共享同一数据
- 引用计数跟踪共享者数量
- 当有修改操作时,才进行实际拷贝
cpp
// 简化的写时拷贝实现
class CowString {
private:
struct StringData {
char* data;
int refCount; // 引用计数
StringData(const char* str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
refCount = 1;
}
~StringData() {
delete[] data;
}
};
StringData* _data;
public:
// 实现细节略...
};
4. 最佳实践和性能建议
4.1 字符串操作优化
cpp
// 不推荐的写法(性能差)
string result;
for (int i = 0; i < 1000; ++i) {
result += "data"; // 可能多次重新分配内存
}
// 推荐的写法
string result;
result.reserve(5000); // 预先分配足够空间
for (int i = 0; i < 1000; ++i) {
result += "data"; // 无重新分配
}
4.2 选择合适的方法
cpp
string str = "hello";
// 尾部添加字符的三种方式
str.push_back('!'); // 方式1
str.append(1, '!'); // 方式2
str += '!'; // 方式3(最常用)
// 查找操作选择
size_t pos1 = str.find('e'); // 查找字符
size_t pos2 = str.find("ll"); // 查找子串
size_t pos3 = str.rfind('l'); // 反向查找
5. 总结
string类是C++中最重要的工具类之一,它:
- 解决了C字符串的安全性问题:自动内存管理,防止缓冲区溢出
- 提供了丰富的操作方法:查找、替换、分割等常用操作一应俱全
- 具有高效的实现:小字符串优化、写时拷贝等技术提升性能
推荐的写法
cpp
string result;
result.reserve(5000); // 预先分配足够空间
for (int i = 0; i < 1000; ++i) {
result += "data"; // 无重新分配
}
4.2 选择合适的方法
cpp
string str = "hello";
// 尾部添加字符的三种方式
str.push_back('!'); // 方式1
str.append(1, '!'); // 方式2
str += '!'; // 方式3(最常用)
// 查找操作选择
size_t pos1 = str.find('e'); // 查找字符
size_t pos2 = str.find("ll"); // 查找子串
size_t pos3 = str.rfind('l'); // 反向查找
5. 总结
string类是C++中最重要的工具类之一,它:
- 解决了C字符串的安全性问题:自动内存管理,防止缓冲区溢出
- 提供了丰富的操作方法:查找、替换、分割等常用操作一应俱全
- 具有高效的实现:小字符串优化、写时拷贝等技术提升性能
- 支持现代C++特性:与STL算法、范围for等完美配合