在C++开发中,string类是最基础也是最常用的容器之一。作为C++标准库的重要组成部分,string封装了字符串的各种操作,极大简化了开发者的工作。但你是否想过,这个看似简单的类背后隐藏着怎样的设计哲学和实现细节?
今天我们就来动手实现一个简化版的string类,重点模拟其增删查改功能。通过这个项目,你不仅能深入理解string的内部机制,还能掌握内存管理、运算符重载等C++核心概念。我在实际开发中发现,很多中级开发者对string的使用停留在表面,一旦遇到性能问题或需要定制功能时就束手无策。这个实现过程将帮你建立底层认知,写出更高效的字符串处理代码。
一个完整的string类需要考虑以下几个核心要素:
我们先定义类的基本结构:
cpp复制class MyString {
public:
// 构造/析构函数
MyString();
MyString(const char* str);
~MyString();
// 容量相关
size_t size() const;
size_t capacity() const;
bool empty() const;
// 修改操作
void push_back(char c);
void append(const char* str);
void insert(size_t pos, const char* str);
void erase(size_t pos, size_t len);
// 访问操作
char& operator[](size_t pos);
const char& operator[](size_t pos) const;
// 查找操作
size_t find(const char* substr) const;
private:
char* _data; // 存储字符串数据
size_t _size; // 当前字符串长度
size_t _capacity; // 当前分配的内存容量
};
string类的核心挑战在于高效的内存管理。我们采用"预分配+按需扩容"的策略:
这种策略平衡了内存使用效率和性能。我在实际项目中测试发现,1.5倍的扩容因子在大多数场景下表现最优,避免了频繁扩容的同时也不会浪费太多内存。
基础构造函数需要处理空字符串和C风格字符串两种情况:
cpp复制MyString::MyString()
: _data(new char[16]), _size(0), _capacity(16) {
_data[0] = '\0';
}
MyString::MyString(const char* str) {
_size = strlen(str);
_capacity = _size + 1;
_data = new char[_capacity];
strcpy(_data, str);
}
MyString::~MyString() {
delete[] _data;
_data = nullptr;
_size = _capacity = 0;
}
注意:析构函数必须释放分配的内存,否则会导致内存泄漏。这也是RAII(资源获取即初始化)原则的体现。
当需要插入新内容但空间不足时,我们需要扩容:
cpp复制void MyString::reserve(size_t new_capacity) {
if (new_capacity <= _capacity) return;
char* new_data = new char[new_capacity];
strcpy(new_data, _data);
delete[] _data;
_data = new_data;
_capacity = new_capacity;
}
这个实现有几个关键点:
插入操作需要考虑多种情况:
cpp复制void MyString::insert(size_t pos, const char* str) {
if (pos > _size) throw std::out_of_range("Invalid position");
size_t len = strlen(str);
if (_size + len >= _capacity) {
reserve((_size + len) * 2); // 2倍扩容
}
// 移动现有字符
memmove(_data + pos + len, _data + pos, _size - pos + 1);
// 插入新内容
memcpy(_data + pos, str, len);
_size += len;
}
提示:使用memmove而不是memcpy来移动内存,因为源和目标区域可能重叠。
删除操作需要考虑:
cpp复制void MyString::erase(size_t pos, size_t len) {
if (pos >= _size) return;
len = std::min(len, _size - pos); // 实际删除长度
// 移动剩余字符
memmove(_data + pos, _data + pos + len, _size - pos - len + 1);
_size -= len;
}
查找子串使用简单的暴力匹配算法,对于学习目的足够:
cpp复制size_t MyString::find(const char* substr) const {
size_t sublen = strlen(substr);
if (sublen == 0) return 0;
if (sublen > _size) return npos;
for (size_t i = 0; i <= _size - sublen; ++i) {
if (strncmp(_data + i, substr, sublen) == 0) {
return i;
}
}
return npos;
}
对于生产环境,可以考虑更高效的算法如KMP或Boyer-Moore,但实现复杂度会显著增加。
提供const和非const版本的下标访问:
cpp复制char& MyString::operator[](size_t pos) {
if (pos >= _size) throw std::out_of_range("Index out of range");
return _data[pos];
}
const char& MyString::operator[](size_t pos) const {
if (pos >= _size) throw std::out_of_range("Index out of range");
return _data[pos];
}
为了支持cout等流输出:
cpp复制std::ostream& operator<<(std::ostream& os, const MyString& str) {
os << str.c_str(); // 假设实现了c_str()方法
return os;
}
现代C++中,移动语义可以显著提升性能:
cpp复制MyString::MyString(MyString&& other) noexcept
: _data(other._data), _size(other._size), _capacity(other._capacity) {
other._data = nullptr;
other._size = other._capacity = 0;
}
MyString& MyString::operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] _data;
_data = other._data;
_size = other._size;
_capacity = other._capacity;
other._data = nullptr;
other._size = other._capacity = 0;
}
return *this;
}
确保操作失败时不会破坏对象状态:
cpp复制void MyString::append(const char* str) {
size_t len = strlen(str);
if (_size + len >= _capacity) {
try {
reserve((_size + len) * 2);
} catch (...) {
throw; // 传播异常,保持原对象不变
}
}
strcpy(_data + _size, str);
_size += len;
}
编写测试用例验证基本功能:
cpp复制void test_basic() {
MyString s1;
assert(s1.empty());
MyString s2("hello");
assert(s2.size() == 5);
s2.append(" world");
assert(s2.size() == 11);
s2.insert(5, " beautiful");
assert(s2.find("beautiful") == 6);
s2.erase(5, 10);
assert(s2.size() == 11);
}
比较自定义string和std::string的性能:
cpp复制void test_performance() {
auto start = std::chrono::high_resolution_clock::now();
MyString s;
for (int i = 0; i < 100000; ++i) {
s.append("test");
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "MyString: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< "ms" << std::endl;
}
症状:程序运行时间越长,内存占用越高。
原因:未正确释放分配的内存,特别是在赋值操作和析构时。
解决方案:
症状:程序崩溃或输出乱码。
原因:未检查下标或位置参数的有效性。
解决方案:
症状:频繁的字符串操作导致性能下降。
原因:不合理的扩容策略或频繁的内存分配。
解决方案:
现代string实现通常对小字符串进行特殊优化,将短字符串直接存储在对象内部,避免堆分配。这种技术称为Small String Optimization(SSO)。实现SSO需要:
Copy-On-Write是另一种优化技术,多个string对象可以共享同一块内存,直到某个对象需要修改内容时才创建副本。实现COW需要:
不过在现代C++中,由于多线程问题,COW的实现变得复杂,许多标准库实现已不再使用这种技术。
对于特定场景,可以定制内存分配策略:
这需要实现allocator概念,并作为模板参数传递给string类。