作为一名C++开发者,我经常被问到关于STL容器底层实现的问题,尤其是string类。很多开发者虽然能熟练使用string的各种接口,但对它的内部工作机制却知之甚少。今天,我将带大家深入剖析string类的底层实现原理,并手把手教你从零开始实现一个简化版的string类。
string作为C++中最常用的容器之一,其底层实现涉及内存管理、深拷贝、迭代器设计等多个核心概念。理解这些原理不仅能帮助你在面试中脱颖而出,更能让你在日常开发中写出更高效、更健壮的代码。
string类的核心由三个成员变量支撑:
cpp复制class string {
private:
char* _str; // 指向动态分配的字符数组
size_t _size; // 当前字符串长度(不包含结尾的'\0')
size_t _capacity; // 当前分配的存储容量
};
这种设计与大多数现代C++实现一致,通过指针管理动态内存,_size记录有效字符数,_capacity记录当前分配的内存大小。这种分离的设计使得string能够高效地支持各种操作,同时保持内存使用的合理性。
无参构造需要创建一个空字符串。初学者常犯的错误是直接将_str设为nullptr:
cpp复制string() : _str(nullptr), _size(0), _capacity(0) {}
这种实现会导致cout等操作崩溃,因为nullptr不是有效的字符串地址。正确的做法是分配一个只包含'\0'的字符数组:
cpp复制string() : _str(new char[1]{'\0'}), _size(0), _capacity(0) {}
带参构造需要考虑字符串长度和内存分配:
cpp复制string(const char* str) {
_size = strlen(str);
_str = new char[_size + 1]; // +1 for '\0'
_capacity = _size;
strcpy(_str, str);
}
这里有几个关键点:
我们可以将无参和带参构造合并为一个全缺省构造函数:
cpp复制string(const char* str = "") {
_size = strlen(str);
_str = new char[_size + 1];
_capacity = _size;
strcpy(_str, str);
}
这种设计更简洁,且能处理所有构造场景。
string需要实现深拷贝以避免多个对象共享同一块内存:
cpp复制string(const string& other) {
_str = new char[other._size + 1];
_size = other._size;
_capacity = other._capacity;
memcpy(_str, other._str, _size + 1);
}
关键点:
赋值操作也需要深拷贝,且要考虑自赋值情况:
cpp复制string& operator=(const string& other) {
if (this != &other) {
char* tmp = new char[other._size + 1];
memcpy(tmp, other._str, other._size + 1);
delete[] _str;
_str = tmp;
_size = other._size;
_capacity = other._capacity;
}
return *this;
}
这种实现先创建临时对象再交换的策略,保证了异常安全性。
重载operator[]提供类似数组的访问方式:
cpp复制char& operator[](size_t pos) {
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const {
assert(pos < _size);
return _str[pos];
}
提供const和非const两个版本,前者用于const对象,后者允许修改字符。
string的迭代器本质是字符指针的封装:
cpp复制typedef char* iterator;
typedef const char* const_iterator;
iterator begin() { return _str; }
iterator end() { return _str + _size; }
const_iterator begin() const { return _str; }
const_iterator end() const { return _str + _size; }
这种设计使得string可以支持范围for循环:
cpp复制for (char c : myString) {
// 处理每个字符
}
迭代器的强大之处在于它为不同容器提供了统一的访问接口,隐藏了底层实现细节。
reserve用于预分配内存,避免频繁扩容:
cpp复制void reserve(size_t n) {
if (n > _capacity) {
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
关键点:
在末尾添加单个字符:
cpp复制void push_back(char ch) {
if (_size == _capacity) {
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
扩容策略:
追加字符串:
cpp复制void append(const char* str) {
size_t len = strlen(str);
if (_size + len > _capacity) {
reserve(max(_size + len, 2 * _capacity));
}
strcpy(_str + _size, str);
_size += len;
}
这里使用max确保当追加的字符串很长时,能一次性分配足够空间。
通过复用push_back和append实现:
cpp复制string& operator+=(char ch) {
push_back(ch);
return *this;
}
string& operator+=(const char* str) {
append(str);
return *this;
}
返回引用支持链式调用:str += 'a' += "bc"。
在指定位置插入字符或字符串:
cpp复制void insert(size_t pos, char ch) {
assert(pos <= _size);
if (_size == _capacity) {
reserve(2 * _capacity);
}
memmove(_str + pos + 1, _str + pos, _size - pos + 1);
_str[pos] = ch;
_size++;
}
void insert(size_t pos, const char* str) {
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity) {
reserve(max(_size + len, 2 * _capacity));
}
memmove(_str + pos + len, _str + pos, _size - pos + 1);
memcpy(_str + pos, str, len);
_size += len;
}
使用memmove而不是memcpy,因为源和目标内存可能重叠。
删除指定位置的字符:
cpp复制void erase(size_t pos, size_t len = npos) {
assert(pos < _size);
if (len == npos || len >= _size - pos) {
_str[pos] = '\0';
_size = pos;
} else {
memmove(_str + pos, _str + pos + len, _size - pos - len + 1);
_size -= len;
}
}
静态成员npos的定义:
cpp复制// 在类定义外
const size_t string::npos = -1;
if (this != &other)。现代string实现常使用SSO(Small String Optimization),对小字符串直接存储在对象内部,避免堆分配。这可以显著提升小字符串的性能。
Copy-On-Write是另一种优化技术,多个string共享同一内存,直到有修改操作时才真正复制。虽然能节省内存,但增加了复杂性,现代实现已较少使用。
string和vector
通过手动实现string类,我们深入理解了:
这种底层理解不仅能帮助你在面试中脱颖而出,更能让你在日常开发中:
在后续文章中,我们将继续实现string的其他功能,如查找、子串、比较操作等,并探讨更高级的优化技术。