在C++编程中,string类是最基础也是最常用的类之一。虽然标准库提供了完善的string实现,但自己动手实现一个简化版的string类,对于理解内存管理、运算符重载和类设计等核心概念非常有帮助。本文将详细解析一个自定义string类的完整实现过程。
我们的string类需要包含三个核心成员变量:
char* _arr:指向动态分配的字符数组int _size:当前字符串长度int _cap:当前分配的容量cpp复制class string {
private:
char* _arr = nullptr;
int _cap = 0;
int _size = 0;
};
这种设计与标准库的string类似,但做了简化。_size表示当前字符串的实际长度,而_cap表示分配的缓冲区大小,通常比_size大一些,以避免频繁的内存重新分配。
注意:在真实项目中,size_t类型比int更适合表示大小和容量,这里使用int是为了简化示例代码。
构造函数需要处理多种初始化情况,包括空字符串、C风格字符串和拷贝构造:
cpp复制// 默认构造函数和C字符串构造函数
string(const char* a1 = "") {
_size = strlen(a1);
_cap = _size + 1;
_arr = new char[_cap];
strcpy(_arr, a1);
}
// 拷贝构造函数
string(const string& a1) {
_arr = new char[a1._cap];
strcpy(_arr, a1._arr);
_cap = a1._cap;
_size = a1._size;
}
// 析构函数
~string() {
delete[] _arr;
_cap = 0;
_size = 0;
}
拷贝构造函数的实现特别重要,它需要深拷贝源字符串的内容,而不是简单地复制指针。这是实现"Rule of Three"(三法则)的关键部分。
动态字符串的核心挑战之一是高效管理内存。我们实现了自动扩容机制:
cpp复制void capcity() {
if (_cap <= _size) {
_cap = 2 * _cap; // 常见的倍增策略
char* arr = new char[_cap + 1];
strcpy(arr, _arr);
delete[] _arr; // 记得释放旧内存
_arr = arr;
}
}
void reserve(const int a) {
if (_cap < a) {
_cap = a;
char* new_arr = new char[_cap];
strcpy(new_arr, _arr);
delete[] _arr;
_arr = new_arr;
}
}
容量管理有几个关键点:
我们实现了多种字符串修改方法,包括追加字符、追加字符串、插入和删除:
cpp复制// 追加单个字符
void operator+=(const char a) {
_size++;
capcity();
_arr[_size - 1] = a;
_arr[_size] = '\0';
}
// 追加C风格字符串
void operator+=(const char* arr) {
size_t len = strlen(arr);
size_t old_size = _size;
_size += len;
capcity();
strcpy(_arr + old_size, arr);
}
// 在指定位置插入字符
void insert(const char a) {
_size++;
capcity();
char* a1 = end() + 1;
while (a1 != begin()) {
*a1 = *(a1 - 1);
a1--;
}
*a1 = a;
}
// 删除子串
void erase(size_t pos, size_t a1 = -1) {
char* arr = _arr + pos;
if (end() - _arr - pos <= a1) {
a1 = end() - _arr - pos;
_arr[pos] = '\0';
_size = pos;
} else {
while (_arr + pos + a1 + 1 != end() + 1) {
_arr[pos] = _arr[pos + a1];
pos++;
}
_size -= a1;
}
}
实操技巧:在实现字符串操作时,要特别注意边界条件处理,如空字符串、越界访问等。例如,insert()中的循环条件
a1 != begin()确保不会越界。
为了让我们的string类用起来更自然,我们重载了几个常用运算符:
cpp复制// 下标访问运算符
char& operator[](int a) {
return _arr[a];
}
// 迭代器支持
iterator begin() { return _arr; }
iterator end() { return _arr + _size; }
下标运算符提供了类似数组的访问方式,而begin()和end()则支持基于范围的for循环。
子串和查找是字符串操作中的常用功能:
cpp复制// 查找字符
int find(char a1, int a2 = 0) {
while (*(_arr + a2) != a1 && *(_arr + a2) != '\0') {
a2++;
}
return *(_arr + a2) == '\0' ? -1 : a2;
}
// 获取子串
string& substr(int pos, size_t len = -1) {
string s2;
if (end() - _arr - pos > len) {
s2._arr = new char[len + 1];
s2._size = len;
s2._cap = len + 1;
memcpy(s2._arr, _arr + pos, len);
s2._arr[len] = '\0';
} else {
len = end() - _arr - pos;
s2._arr = new char[len + 1];
s2._size = len;
s2._cap = len + 1;
memcpy(s2._arr, _arr + pos, len);
s2._arr[len] = '\0';
}
return s2;
}
substr()的实现有几个关键点:
在手写string类时,内存管理是最容易出错的地方。常见问题包括:
避坑指南:使用RAII原则管理资源,可以考虑使用std::unique_ptr来辅助管理内存,但会稍微增加复杂度。
当前实现有几个可以优化的地方:
测试string类时需要覆盖各种边界情况:
cpp复制// 示例测试用例
void test_string() {
liang::string s1; // 默认构造
assert(s1.print() == std::string(""));
liang::string s2("hello"); // C字符串构造
assert(s2.print() == std::string("hello"));
s2 += '!'; // 追加字符
assert(s2.print() == std::string("hello!"));
liang::string s3 = s2.substr(1, 3); // 子串
assert(s3.print() == std::string("ell"));
s2.erase(1, 2); // 删除
assert(s2.print() == std::string("hlo!"));
}
实现一个完整的string类是理解C++面向对象编程和资源管理绝佳的练习。虽然标准库中的string已经非常完善,但通过自己实现,可以深入理解其背后的设计思想和实现细节。在实际项目中,除非有特殊需求,否则建议直接使用std::string,它经过了充分的优化和测试。