📌 引言
C++标准库中的std::string
是日常开发中最常用的类之一,但你是否好奇它的底层实现?本文将带你从零实现一个简化版string
类(命名空间tyx
),覆盖构造、拷贝、动态扩容、运算符重载等核心功能,并分析常见陷阱与优化方法。
适合人群:C++初学者、面试备战者、对STL底层感兴趣的开发者。
🔧 核心实现解析
1. 基础结构与构造函数
成员变量
cpp
private:
size_t _size; // 当前字符串长度
size_t _capacity; // 当前分配的内存容量
char* _str; // 动态分配的字符数组
static size_t npos; // 特殊标识(类似std::string::npos)
- 初始化顺序 :成员变量声明顺序需与初始化列表一致,否则可能引发未定义行为(如先初始化
_str
再初始化_capacity
会导致越界访问)。
全缺省构造函数
cpp
string(const char* str = "")
:_size(strlen(str)), _capacity(_size), _str(new char[_capacity + 1]) {
memcpy(_str, str, _size + 1); // 比strcpy更安全(避免\0截断)
}
- 陷阱 :
str
默认值不能为nullptr
或'\0'
,否则strlen
会崩溃。空字符串""
是最优选择。
2. 现代C++技巧:拷贝构造与赋值
传统写法(易错)
cpp
// 深拷贝:需手动管理内存
string(const string& s) {
_str = new char[s._capacity + 1];
memcpy(_str, s._str, s._size + 1);
_size = s._size;
_capacity = s._capacity;
}
现代写法(推荐)
cpp
// 利用临时对象+swap:异常安全且简洁
string(const string& s) : _str(nullptr), _size(0), _capacity(0) {
string tmp(s._str); // 复用构造函数
swap(tmp); // 交换资源
}
// 赋值运算符(参数为值传递,自动调用拷贝构造)
string& operator=(string tmp) {
swap(tmp); // 交换后tmp自动析构旧资源
return *this;
}
- 优势 :避免代码重复,天然处理自赋值问题(如
s = s
)。
3. 动态内存管理
reserve()扩容策略
cpp
void reserve(size_t n) {
if (n > _capacity) {
char* tmp = new char[n + 1]; // 多1位存放\0
memcpy(tmp, _str, _size + 1);
delete[] _str; // 释放旧内存
_str = tmp;
_capacity = n;
}
}
优化点 :push_back
时采用2倍扩容(减少频繁分配):
cpp
reserve(_capacity == 0 ? 4 : _capacity * 2);
4. 流操作符重载
流插入(<<)
cpp
ostream& operator<<(ostream& out, const tyx::string& s) {
for (auto ch : s) { // 支持范围for(需实现begin/end)
out << ch;
}
return out;
}
流提取(>>)优化
cpp
istream& operator>>(istream& in, tyx::string& s) {
s.clear();
char buff[128]; // 缓冲区减少扩容次数
size_t i = 0;
// ...(略去跳过空白字符逻辑)
while (ch != ' ' && ch != '\n') {
buff[i++] = ch;
if (i == 127) { // 缓冲区满时批量追加
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
// 处理剩余字符
if (i > 0) {
buff[i] = '\0';
s += buff;
}
return in;
}
- 性能对比 :缓冲区减少
+=
操作的内存分配次数,提升输入效率。
5.比较运算符的实现需求
我们需要实现以下6个比较运算符:
==
(等于)!=
(不等于)<
(小于)<=
(小于等于)>
(大于)>=
(大于等于)
目标:
- 正确性:严格遵循字典序比较规则。
- 高效性:避免重复计算,利用短路逻辑优化。
- 复用性 :通过复用
==
和<
减少代码冗余。
2. 关键实现代码解析
2.1 等于运算符(==)
cpp
bool operator==(const string& s) const {
return _size == s._size &&
memcmp(_str, s._str, _size) == 0;
}
- 逻辑 :
- 先比较长度,长度不等直接返回
false
。 - 长度相等时,用
memcmp
逐字节比较内容。
- 先比较长度,长度不等直接返回
- 优化点 :
memcmp
比逐字符比较更快(编译器可能内联优化)。- 短路逻辑:若
_size != s._size
,直接跳过memcmp
。
2.2 小于运算符(<)
cpp
bool operator<(const string& s) const {
int ret = memcmp(_str, s._str,
_size < s._size ? _size : s._size);
return ret == 0 ? _size < s._size : ret < 0;
}
- 逻辑 :
- 比较共同长度内的内容(
memcmp
返回值为-1/0/1
)。 - 若共同部分相等,则长度更小的字符串更"小"。
- 比较共同长度内的内容(
- 示例 :
"abc" < "abcd"
→ 共同部分"abc"
相等,比较长度。"abx" < "aby"
→memcmp
在'x'
和'y'
处返回-1
。
2.3 其他运算符的复用
通过复用==
和<
,其余运算符可一行实现:
cpp
bool operator!=(const string& s) const { return !(*this == s); }
bool operator<=(const string& s) const { return *this < s || *this == s; }
bool operator>(const string& s) const { return !(*this <= s); }
bool operator>=(const string& s) const { return !(*this < s); }
- 优势 :
- 减少代码重复,维护更简单。
- 逻辑清晰,避免隐式错误。
3. 性能优化与陷阱
3.1 为什么不用strcmp
?
strcmp
依赖\0
终止符,而string
可能包含\0
(如二进制数据)。memcmp
直接按字节比较,更安全且无需遍历到\0
。
3.2 短路逻辑的重要性
cpp
// 低效写法(无短路)
bool operator==(const string& s) const {
if (_size != s._size) return false;
for (size_t i = 0; i < _size; ++i) {
if (_str[i] != s._str[i]) return false;
}
return true;
}
- 问题:即使长度不等,仍会遍历整个字符串。
- 修复:优先比较长度(如2.1节的实现)。
3.3 处理空字符串的边界条件
- 空字符串(
""
)应满足:"" == ""
为true
。"" < "a"
为true
。
- 通过
_size
和memcmp
的联合检查可自然覆盖。
4. 测试用例验证
cpp
void test_comparisons() {
tyx::string s1 = "apple";
tyx::string s2 = "banana";
tyx::string s3 = "apple";
tyx::string s4 = "app";
assert(s1 < s2); // "apple" < "banana"
assert(s1 == s3); // "apple" == "apple"
assert(s4 < s1); // "app" < "apple"
assert(s1 != s4); // "apple" != "app"
assert(s2 > s1); // "banana" > "apple"
}
- 覆盖场景 :
- 相等字符串、前缀相同字符串、完全不同字符串。
- 空字符串与其他字符串的比较。
🚀 关键问题与优化
1. 插入与删除的边界处理
- insert :需校验
pos <= _size
,并处理npos
(表示插入到末尾)。 - erase :区分
len = npos
(删除到末尾)和len + pos >= _size
的情况。
2. 查找函数优化
cpp
size_t find(const char* str, size_t pos = 0) {
const char* ptr = strstr(_str + pos, str); // 复用标准库strstr
return ptr ? ptr - _str : npos;
}
性能 :strstr
通常经过高度优化,比手动遍历更高效
🎯 总结
- 现代C++风格 :善用
swap
减少代码冗余,提升异常安全性。 - 性能优化 :缓冲区、2倍扩容、
memcpy
替代strcpy
。 - 扩展方向:实现移动语义(C++11)、小型字符串优化(SSO)。
- 核心技巧 :
- 优先比较长度,利用
memcmp
加速内容比较。 - 通过复用
==
和<
简化其他运算符的实现。
- 优先比较长度,利用
- 避坑指南 :
- 避免
strcmp
(不兼容含\0
的字符串)。 - 始终校验边界条件(如空字符串)
- 避免