1. C++ string类模拟实现指南
在C++编程中,string类是最常用的标准库组件之一。作为一位有多年C++开发经验的工程师,我深知理解string类的内部实现机制对于提升编程能力的重要性。本文将带你从零开始,完整实现一个简化版的string类,涵盖构造、遍历、修改等核心功能。
1.1 string的构造实现
1.1.1 默认构造函数陷阱
新手最容易犯的错误就是在默认构造函数中直接将字符串指针初始化为nullptr。让我们看一个典型的错误示例:
cpp复制class String {
public:
String() : _str(nullptr), _size(0), _capacity(0) {}
const char* c_str() const {
return _str; // 危险!可能返回nullptr
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
这种实现的问题在于:当调用c_str()后使用cout输出时,程序会崩溃。原因是cout的<<操作符会尝试读取指针指向的内存内容,而访问nullptr是未定义行为。
关键经验:标准库的string即使为空,也会至少包含一个'\0'字符。这是保证安全性的重要设计。
1.1.2 带参构造的优化技巧
带参构造函数的常见实现方式是:
cpp复制String(const char* str) {
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
但这种实现有个性能问题:strlen()需要遍历整个字符串,而我们在后续的strcpy()中又遍历了一次。更高效的写法是:
cpp复制String(const char* str)
: _size(strlen(str))
, _capacity(_size)
, _str(new char[_capacity + 1])
{
strcpy(_str, str);
}
不过这里有个隐藏陷阱:成员变量初始化顺序是按照类中声明的顺序,而不是初始化列表中的顺序。如果声明顺序是_str在前,_size在后,那么_str会先被初始化,此时_size还是随机值。
1.1.3 全缺省构造函数的最佳实践
我们可以将默认构造和带参构造合并为一个全缺省构造函数:
cpp复制String(const char* str = "")
: _size(strlen(str))
, _capacity(_size)
, _str(new char[_capacity + 1])
{
strcpy(_str, str);
}
这里的关键点是缺省参数不能给nullptr,而应该给空字符串""。因为:
- strlen(nullptr)会导致程序崩溃
- 空字符串自动包含'\0',符合C字符串规范
1.2 析构函数实现
析构函数的实现相对简单,但有几个注意事项:
cpp复制~String() {
delete[] _str; // 必须使用delete[]匹配new[]
_str = nullptr; // 良好的编程习惯
_size = _capacity = 0;
}
重要提示:即使_str为nullptr,delete[] nullptr也是安全的,这是C++标准规定的。
1.3 string的遍历与修改
1.3.1 下标访问实现
string需要提供类似数组的下标访问功能,通常需要实现两个版本:
cpp复制// 普通版本,允许修改
char& operator[](size_t pos) {
assert(pos < _size);
return _str[pos];
}
// const版本,用于const对象
const char& operator[](size_t pos) const {
assert(pos < _size);
return _str[pos];
}
这种设计模式称为"const重载",是C++中处理const对象的常用技术。
1.3.2 迭代器实现
现代C++更推荐使用迭代器进行遍历。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; }
这种实现使得我们可以使用范围for循环:
cpp复制String s("hello");
for(char ch : s) {
cout << ch;
}
值得注意的是,标准库的实现通常更复杂,使用专门的迭代器类,但通过typedef对外暴露统一的接口名称。
1.4 容量管理:push_back和append
1.4.1 push_back实现
push_back需要在末尾添加一个字符,涉及容量检查:
cpp复制void push_back(char ch) {
if(_size == _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size++] = ch;
_str[_size] = '\0'; // 必须手动添加结束符
}
这里的关键点是扩容策略:初始给4个字符空间,之后每次翻倍。这种策略在时间和空间效率上取得了很好的平衡。
1.4.2 reserve实现
reserve负责容量调整,是string类的核心函数:
cpp复制void reserve(size_t n) {
if(n > _capacity) {
char* tmp = new char[n + 1]; // +1给'\0'
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
常见错误:
- 忘记拷贝原内容
- 忘记释放旧内存
- 忘记更新_capacity
- 忘记预留'\0'的空间
1.4.3 append实现
append用于添加字符串,需要考虑更复杂的扩容策略:
cpp复制void append(const char* str) {
size_t len = strlen(str);
if(_size + len > _capacity) {
// 智能扩容策略
reserve(_capacity * 2 > _size + len ? _capacity * 2 : _size + len);
}
strcpy(_str + _size, str);
_size += len;
}
这里的扩容策略是:
- 如果需要的空间小于当前容量的两倍,则按两倍扩容
- 否则按实际需要扩容
这种策略减少了频繁扩容的开销,同时保证了大数据量的处理能力。
1.5 operator+=的实现
+=运算符可以复用已有的push_back和append:
cpp复制String& operator+=(char ch) {
push_back(ch);
return *this;
}
String& operator+=(const char* str) {
append(str);
return *this;
}
这种实现体现了DRY(Don't Repeat Yourself)原则,减少了代码重复。
2. 完整实现与测试
2.1 完整类定义
cpp复制class String {
public:
typedef char* iterator;
typedef const char* const_iterator;
// 构造与析构
String(const char* str = "");
~String();
// 迭代器
iterator begin() { return _str; }
iterator end() { return _str + _size; }
const_iterator begin() const { return _str; }
const_iterator end() const { return _str + _size; }
// 容量操作
void reserve(size_t n);
void push_back(char ch);
void append(const char* str);
// 运算符重载
char& operator[](size_t pos);
const char& operator[](size_t pos) const;
String& operator+=(char ch);
String& operator+=(const char* str);
// 其他接口
size_t size() const { return _size; }
size_t capacity() const { return _capacity; }
const char* c_str() const { return _str; }
private:
char* _str;
size_t _size;
size_t _capacity;
};
2.2 测试用例
cpp复制void test() {
// 构造测试
String s1;
String s2("hello");
String s3 = "world";
// 遍历测试
for(size_t i = 0; i < s2.size(); ++i) {
cout << s2[i];
}
cout << endl;
for(char ch : s3) {
cout << ch;
}
cout << endl;
// 修改测试
s2.push_back('!');
s3 += "!!!";
// 容量测试
String s4;
for(int i = 0; i < 100; ++i) {
s4.push_back('a');
}
cout << s4.size() << " " << s4.capacity() << endl;
}
3. 常见问题与优化建议
3.1 性能优化点
- 小字符串优化(SSO):标准库实现通常会为短字符串(通常16字节以内)使用栈空间,避免堆分配
- 移动语义:C++11后应实现移动构造和移动赋值,避免不必要的拷贝
- 写时复制(COW):某些实现会使用引用计数实现写时复制,但现代实现多已弃用
3.2 常见错误排查
- 访问越界:所有下标访问必须检查边界
- 空指针解引用:确保c_str()不会返回nullptr
- 内存泄漏:确保每个new都有对应的delete
- 浅拷贝问题:需要实现拷贝构造和赋值运算符
3.3 扩展接口建议
- find系列函数:实现字符串查找功能
- insert/erase:实现中间插入删除
- 比较运算符:实现字符串比较
- IO操作:重载<<和>>运算符
通过这个简化版的string实现,我们深入理解了字符串类的核心机制。在实际项目中,建议直接使用标准库的string,但了解其实现原理对于提升C++编程能力至关重要。