.
💓 博客主页:倔强的石头的CSDN主页
📝Gitee主页:倔强的石头的gitee主页
⏩ 文章专栏:《C++指南》
期待您的关注
引言
在前文中,我们深入探讨了C++标准库中
basic_string
的成员变量、默认成员函数及常用操作。
本文作为系列第三篇,将结合模拟实现的代码,逐行解析basic_string
的底层原理,涵盖构造函数、拷贝控制、容量管理、修改操作等核心功能的实现细节与优化技巧。
通过手写一个简化版string
类,帮助读者彻底理解std::string
的内部工作机制。
一、成员变量与内存管理
1.1 核心成员变量
标准库的basic_string
通过三个核心变量管理字符串:
- 字符指针
_str
:指向动态分配的字符数组。 - 当前长度
_size
:字符串有效字符个数(不含\0
)。 - 总容量
_capacity
:当前内存可容纳的最大字符数(含\0
)。
模拟实现代码:
namespace xc {
class string {
private:
char* _str; // 字符存储指针
size_t _size; // 有效字符数
size_t _capacity; // 总容量(含\0)
public:
static const size_t npos = -1; // 特殊标记
};
}
1.2 内存分配策略
- 默认构造:初始化为空字符串(
_str
指向\0
)。 注意不能初始化为nullptr,否则调用c_str时,就会对空指针解引用 - 动态扩容:当
_size
达到_capacity
时,按2倍或需求大小扩容,避免频繁内存分配。
构造函数实现:
// 默认构造(支持传入C字符串)
string::string(const char* str) : _size(strlen(str)) {
_str = new char[_size + 1]; // 多分配1字节存放\0
strcpy(_str, str);
_capacity = _size; // 初始容量等于长度
}
二、默认成员函数的实现与优化
2.1 拷贝构造函数
传统写法需要手动分配内存并拷贝数据,而现代C++写法通过“构造临时对象 + 交换资源”简化代码:
(关于swap函数的实现可跳转6.5查找)
// 传统写法(易错且冗余)
string::string(const string& s) {
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 现代写法(利用临时对象)
string::string(const string& s) {
string tmp(s._str); // 调用构造函数
swap(tmp); // 交换资源
}
2.2 赋值运算符重载
通过**“拷贝构造临时对象 + 交换”**避免自赋值问题,同时减少重复代码:
//传统写法
string& string::operator=(const string& s)
{
if (this != &s)
{
delete[] _str;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
// 优化版赋值重载
string& string::operator=(const string& s) {
if (this != &s) { // 防止自赋值
string tmp(s); // 调用拷贝构造
swap(tmp); // 交换资源
}
return *this;
}
2.3 析构函数
释放动态内存并将成员变量归零:
string::~string() {
delete[] _str; // 释放堆内存
_size = 0;
_capacity = 0;
}
三、迭代器与元素访问
3.1 迭代器实现
模拟原生指针的行为,提供begin()
和end()
:
using iterator = char*;
iterator begin() { return _str; }
iterator end() { return _str + _size; }
3.2 运算符重载
通过operator[]
提供随机访问,并使用assert
检查越界:
char& operator[](size_t i) {
assert(i < _size); // 越界检查
return _str[i];
}
四、容量管理
4.1 reserve:预分配内存
若需求容量大于当前容量,重新分配内存并拷贝数据:
void string::reserve(size_t n) {
if (n > _capacity) {
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str; // 释放旧内存
_str = tmp;
_capacity = n; // 更新容量
}
}
4.2 resize:调整字符串长度
根据新长度截断或填充字符:
void string::resize(size_t n, char c) {
if (n < _size) {
_str[n] = '\0'; // 截断
_size = n;
} else {
reserve(n); // 确保容量足够
for (size_t i = _size; i < n; ++i) {
_str[i] = c; // 填充字符
}
_size = n;
_str[_size] = '\0';
}
}
五、修改操作
5.1 清空字符串:clear
清空字符串内容但不释放内存(保留容量):
void string::clear() {
_str[0] = '\0'; // 首字符置为结束符
_size = 0; // 长度归零
}
5.2 push_back与append
- 尾插字符:检查扩容后直接写入:
void string::push_back(char c) {
if (_size == _capacity) {
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size++] = c;
_str[_size] = '\0';
}
- 追加字符串:计算长度后扩容并拷贝:
void string::append(const char* str) {
size_t len = strlen(str);
if (_size + len > _capacity) {
reserve(_size + len); // 按需扩容
}
strcpy(_str + _size, str); // 直接拷贝
_size += len;
}
5.3 insert与erase
- 插入字符:移动后续字符腾出位置:
string& string::insert(size_t pos, char c) {
assert(pos <= _size);
if (_size == _capacity) reserve(2 * _capacity);
size_t end = _size + 1;
while (end > pos) { // 从后向前移动
_str[end] = _str[end - 1];
end--;
}
_str[pos] = c;
_size++;
return *this;
}
- 删除字符:覆盖后续字符并更新长度:
string& string::erase(size_t pos, size_t len) {
assert(pos < _size);
if (len == npos || len > _size - pos) {
_str[pos] = '\0';
_size = pos;
} else {
strcpy(_str + pos, _str + pos + len); // 覆盖删除区域
_size -= len;
}
return *this;
}
六、其他关键函数实现
6.1 查找函数:find
查找字符
size_t string::find(char c, size_t pos) const {
assert(pos < _size);
for (size_t i = pos; i < _size; ++i) {
if (_str[i] == c) return i;
}
return npos; // 未找到返回特殊标记
}
查找子串
利用标准库的strstr
函数优化子串查找:
size_t string::find(const char* s, size_t pos) const {
assert(pos < _size);
const char* ptr = strstr(_str + pos, s); // 直接调用C库函数
return ptr ? ptr - _str : npos;
}
6.2 子串生成:substr
截取从pos
开始的len
个字符生成新字符串:
string string::substr(size_t pos, size_t len) const {
assert(pos <= _size);
len = (len == npos) ? _size - pos : len; // 默认取到末尾
len = std::min(len, _size - pos); // 防止越界
string result;
result.reserve(len); // 预分配内存
for (size_t i = 0; i < len; ++i) {
result += _str[pos + i]; // 逐字符追加
}
return result;
}
6.3 流运算符重载
流插入(operator<<
)
直接遍历输出有效字符:
ostream& operator<<(ostream& os, const xc::string& s) {
for (size_t i = 0; i < s.size(); ++i) {
os << s[i]; // 支持链式调用
}
return os;
}
流提取(operator>>
)
优化版输入,通过缓冲区减少扩容次数:
istream& operator>>(istream& is, xc::string& s) {
s.clear(); // 清空原内容
char buff[256]; // 局部缓冲区
char ch;
int idx = 0;
while (is.get(ch) && !isspace(ch)) {
buff[idx++] = ch;
if (idx == 255) { // 缓冲区满时批量追加
buff[idx] = '\0';
s += buff;
idx = 0;
}
}
if (idx > 0) { // 处理剩余字符
buff[idx] = '\0';
s += buff;
}
return is;
}
6.4 比较运算符重载
等于与不等于
bool string::operator==(const string& s) const {
return strcmp(_str, s._str) == 0; // 直接比较C字符串
}
bool string::operator!=(const string& s) const {
return !(*this == s); // 复用等于运算符
}
大小比较
bool string::operator<(const string& s) const {
return strcmp(_str, s._str) < 0; // 字典序比较
}
bool string::operator<=(const string& s) const {
return (*this < s) || (*this == s); // 组合逻辑
}
bool string::operator>(const string& s) const {
return !(*this <= s);
}
bool string::operator>=(const string& s) const {
return !(*this < s);
}
6.5 交换函数:swap
高效交换两个字符串的资源(避免深拷贝):
void string::swap(string& s) {
std::swap(_str, s._str); // 交换指针
std::swap(_size, s._size); // 交换长度
std::swap(_capacity, s._capacity); // 交换容量
}
七、性能优化与注意事项
substr
的优化:- 避免直接使用
new
和strcpy
,通过reserve
预分配内存减少扩容次数。 - 若需要高性能,可实现“浅拷贝+引用计数”(需处理写时复制逻辑)。
- 避免直接使用
find
的局限性:- 当前实现为暴力匹配,标准库可能使用更高效的算法(如KMP)。
流提取的安全性:
- 缓冲区大小固定为256,若输入过长可能丢失数据,可动态调整缓冲区大小。
swap
的优势:- 仅交换指针和元数据,时间复杂度为
O(1)
,适合频繁交换场景。
- 仅交换指针和元数据,时间复杂度为
结语
通过手写string
类,我们深入理解了basic_string
的底层机制。标准库的实现在此基础上进行了大量优化(如SSO、内存池),但核心逻辑与本文的模拟实现高度一致。掌握这些原理后,读者可以更高效地使用std::string
,并能在需要时定制自己的字符串类。
相关阅读
关注博主,第一时间获取更新!