1. 从C风格字符串到C++ string类的进化之路
在C++编程的早期岁月里,处理文本简直就是一场噩梦。我们不得不面对那些以'\0'结尾的字符数组,小心翼翼地计算着内存大小,稍有不慎就会引发缓冲区溢出或者内存泄漏。直到1998年标准模板库(STL)的出现,string类才真正将我们从这种痛苦中解救出来。
string类本质上是一个封装了字符序列的容器,它自动管理内存分配和释放,提供了丰富的操作方法,让字符串处理变得前所未有的简单。想象一下,你不再需要手动计算字符串长度,不再担心拼接时的内存越界,甚至可以直接用"+"运算符连接字符串——这就是现代C++带给我们的便利。
2. string类接口全解析:从构造到销毁
2.1 构造函数:多种初始化方式
string类提供了7种不同的构造函数,满足各种初始化需求:
cpp复制string str1; // 默认构造,空字符串
string str2("Hello"); // C风格字符串初始化
string str3(str2); // 拷贝构造
string str4(5, 'A'); // 填充构造,5个'A'
string str5(str2, 1, 3); // 子串构造,从位置1开始取3个字符
string str6(str2.begin(), str2.begin()+3); // 迭代器范围构造
string str7(std::move(str2)); // 移动构造(C++11)
注意:移动构造后,源对象str2将处于有效但未定义的状态,不应再使用其值。
2.2 容量操作:内存管理的艺术
string类内部使用动态数组存储字符,容量相关方法让我们可以精细控制内存使用:
cpp复制str.empty(); // 判断是否为空
str.size(); // 返回字符数(同length())
str.capacity(); // 返回当前分配的内存大小
str.reserve(100); // 预分配至少100字节内存
str.shrink_to_fit(); // 请求减少capacity到size()大小(C++11)
一个常见的优化技巧是,在已知最终大小时提前reserve,避免多次重新分配:
cpp复制string result;
result.reserve(1000); // 预先分配足够空间
for(int i=0; i<100; ++i) {
result += "some data"; // 现在这些拼接操作不会导致重新分配
}
2.3 元素访问:安全与效率的权衡
访问单个字符有多种方式,各有优缺点:
cpp复制str[0]; // 不检查越界,效率最高
str.at(0); // 会检查越界,越界抛异常
str.front(); // 首字符(C++11)
str.back(); // 末字符(C++11)
str.data(); // 返回C风格字符数组(C++17前不保证以'\0'结尾)
str.c_str(); // 保证以'\0'结尾的C风格字符串
重要区别:operator[]在调试模式下可能有断言检查,但release模式下无检查;at()在任何情况下都会检查边界,性能略低但更安全。
3. string类操作全攻略:从基础到高级
3.1 修改操作:改变字符串内容
string提供了丰富的修改方法,让字符串操作变得直观:
cpp复制// 追加操作
str.append(" world"); // 追加字符串
str += "!"; // 运算符重载追加
str.push_back('!'); // 追加单个字符
// 插入操作
str.insert(5, " beautiful"); // 在位置5插入
// 删除操作
str.erase(5, 10); // 从位置5开始删除10个字符
str.clear(); // 清空字符串
// 替换操作
str.replace(0, 5, "Hi"); // 替换位置0开始的5个字符为"Hi"
一个实用的技巧是使用replace实现简单的模板替换:
cpp复制string template = "Hello {name}, welcome to {city}!";
template.replace(template.find("{name}"), 6, "Alice");
template.replace(template.find("{city}"), 6, "New York");
3.2 字符串操作:查找与子串
查找功能是string类最强大的特性之一:
cpp复制size_t pos = str.find("world"); // 查找子串
if(pos != string::npos) {
// 找到处理
}
pos = str.rfind("l"); // 反向查找
pos = str.find_first_of("aeiou"); // 查找任何元音字母
pos = str.find_last_not_of(" \t\n"); // 查找最后一个非空白字符
string sub = str.substr(6, 5); // 从位置6开始取5个字符的子串
3.3 比较操作:判断字符串关系
比较字符串有多种方式:
cpp复制if(str1 == str2) {...} // 运算符重载比较
if(str1.compare(str2) == 0) {...} // compare方法
if(str1.compare(0, 3, str2, 0, 3) == 0) {...} // 比较子串
性能提示:对于简单相等比较,==运算符通常比compare()更快,因为它可以先比较长度。
4. string类模拟实现:深入理解内部机制
4.1 基础结构设计
让我们从最基本的类定义开始:
cpp复制class MyString {
public:
// 构造函数和析构函数
MyString();
MyString(const char* str);
MyString(const MyString& other);
~MyString();
// 赋值操作
MyString& operator=(const MyString& other);
MyString& operator=(const char* str);
// 容量操作
size_t size() const;
size_t capacity() const;
bool empty() const;
void reserve(size_t new_cap);
// 元素访问
char& operator[](size_t pos);
const char& operator[](size_t pos) const;
char& at(size_t pos);
// 修改操作
void append(const char* str);
MyString& operator+=(const char* str);
void insert(size_t pos, const char* str);
void erase(size_t pos, size_t len);
private:
char* m_data; // 存储字符串数据
size_t m_size; // 当前字符串长度
size_t m_capacity; // 当前分配的内存大小
void reallocate(size_t new_cap); // 内部内存重新分配
};
4.2 关键方法实现
让我们看看几个核心方法的实现细节:
构造函数实现:
cpp复制MyString::MyString(const char* str)
: m_data(nullptr), m_size(0), m_capacity(0) {
if(str) {
m_size = strlen(str);
reserve(m_size + 1); // +1 for '\0'
strcpy(m_data, str);
}
}
拷贝赋值运算符:
cpp复制MyString& MyString::operator=(const MyString& other) {
if(this != &other) { // 防止自赋值
char* new_data = new char[other.m_capacity];
strcpy(new_data, other.m_data);
delete[] m_data;
m_data = new_data;
m_size = other.m_size;
m_capacity = other.m_capacity;
}
return *this;
}
内存重新分配:
cpp复制void MyString::reallocate(size_t new_cap) {
if(new_cap <= m_capacity) return;
// 通常采用1.5倍或2倍增长策略
size_t new_capacity = (m_capacity == 0) ? 16 : m_capacity * 2;
if(new_capacity < new_cap) {
new_capacity = new_cap;
}
char* new_data = new char[new_capacity];
if(m_data) {
strcpy(new_data, m_data);
delete[] m_data;
}
m_data = new_data;
m_capacity = new_capacity;
}
append方法实现:
cpp复制void MyString::append(const char* str) {
if(!str) return;
size_t append_len = strlen(str);
if(m_size + append_len + 1 > m_capacity) {
reserve(m_size + append_len + 1);
}
strcpy(m_data + m_size, str);
m_size += append_len;
}
5. 高级话题与性能优化
5.1 短字符串优化(SSO)
现代string实现通常使用短字符串优化(SSO),对小字符串直接存储在对象内部,避免堆分配:
cpp复制class MyString {
union {
struct {
char* ptr;
size_t size;
size_t capacity;
} long_str;
char short_str[16]; // 假设本地缓冲区16字节
};
bool is_short; // 标记是否使用短字符串优化
};
这种优化可以显著提升小字符串操作的性能,减少内存分配次数。
5.2 写时复制(COW)技术
一些实现使用写时复制技术来优化拷贝操作:
cpp复制class MyString {
struct StringData {
char* data;
size_t size;
size_t capacity;
int refcount; // 引用计数
};
StringData* m_data;
// 拷贝构造时不复制数据,只增加引用计数
MyString(const MyString& other) : m_data(other.m_data) {
++m_data->refcount;
}
// 在需要修改时创建副本
void detach() {
if(m_data->refcount > 1) {
StringData* new_data = copy_data(m_data);
--m_data->refcount;
m_data = new_data;
}
}
};
注意:现代C++标准库实现通常不再使用COW,因为多线程环境下需要额外的同步开销。
5.3 移动语义支持(C++11)
C++11引入的移动语义可以大幅提升string的性能:
cpp复制MyString(MyString&& other) noexcept
: m_data(other.m_data), m_size(other.m_size), m_capacity(other.m_capacity) {
other.m_data = nullptr;
other.m_size = 0;
other.m_capacity = 0;
}
MyString& operator=(MyString&& other) noexcept {
if(this != &other) {
delete[] m_data;
m_data = other.m_data;
m_size = other.m_size;
m_capacity = other.m_capacity;
other.m_data = nullptr;
other.m_size = 0;
other.m_capacity = 0;
}
return *this;
}
6. 常见问题与解决方案
6.1 性能陷阱:不必要的临时对象
cpp复制string s1 = "Hello";
string s2 = "World";
string s3 = s1 + " " + s2; // 创建多个临时对象
优化方案:
cpp复制string s3;
s3.reserve(s1.size() + s2.size() + 1);
s3 += s1;
s3 += " ";
s3 += s2;
6.2 迭代器失效问题
修改字符串可能导致迭代器失效:
cpp复制string str = "hello";
auto it = str.begin();
str.erase(0, 1); // 删除第一个字符
// 此时it可能已经失效
安全做法是在修改后重新获取迭代器。
6.3 多字节字符处理
string本质上是字节序列,处理UTF-8等多字节编码时需要小心:
cpp复制string utf8 = "你好";
cout << utf8.length(); // 输出可能是6而不是2
对于真正的Unicode处理,可以考虑使用wstring或第三方库如ICU。
7. 现代C++中的string_view(C++17)
C++17引入的string_view提供了对字符串的非拥有视图,避免了不必要的拷贝:
cpp复制void process_string(std::string_view sv) {
// 可以接受string、char*等各种形式的字符串
// 且不会产生拷贝开销
}
string long_str = "...";
process_string(long_str); // 无拷贝
process_string("literal"); // 无临时string创建
string_view特别适合作为函数参数,但要注意其生命周期管理——它不拥有底层数据。
