【C++指南】string(三):basic_string底层原理与模拟实现详解
.💓 博客主页:倔强的石头的ZEEKLOG主页
📝Gitee主页:倔强的石头的gitee主页
⏩ 文章专栏:《C++指南》
期待您的关注
文章目录
引言
在前文中,我们深入探讨了C++标准库中basic_string的成员变量、默认成员函数及常用操作。
本文作为系列第三篇,将结合模拟实现的代码,逐行解析basic_string的底层原理,涵盖构造函数、拷贝控制、容量管理、修改操作等核心功能的实现细节与优化技巧。
通过手写一个简化版string类,帮助读者彻底理解std::string的内部工作机制。
一、成员变量与内存管理
1.1 核心成员变量
标准库的basic_string通过三个核心变量管理字符串:
- 字符指针
_str:指向动态分配的字符数组。 - 当前长度
_size:字符串有效字符个数(不含\0)。 - 总容量
_capacity:当前内存可容纳的最大字符数(含\0)。
模拟实现代码:
namespace xc {classstring{private:char* _str;// 字符存储指针 size_t _size;// 有效字符数 size_t _capacity;// 总容量(含\0)public:staticconst size_t npos =-1;// 特殊标记};}1.2 内存分配策略
- 默认构造:初始化为空字符串(
_str指向\0)。注意不能初始化为nullptr,否则调用c_str时,就会对空指针解引用 - 动态扩容:当
_size达到_capacity时,按2倍或需求大小扩容,避免频繁内存分配。
构造函数实现:
// 默认构造(支持传入C字符串) string::string(constchar* str):_size(strlen(str)){ _str =newchar[_size +1];// 多分配1字节存放\0strcpy(_str, str); _capacity = _size;// 初始容量等于长度}二、默认成员函数的实现与优化
2.1 拷贝构造函数
传统写法需要手动分配内存并拷贝数据,而现代C++写法通过“构造临时对象 + 交换资源”简化代码:
(关于swap函数的实现可跳转6.5查找)
// 传统写法(易错且冗余) string::string(const string& s){ _str =newchar[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 =newchar[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 =newchar[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(constchar* 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(constchar* s, size_t pos)const{assert(pos < _size);constchar* 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{returnstrcmp(_str, s._str)==0;// 直接比较C字符串}bool string::operator!=(const string& s)const{return!(*this== s);// 复用等于运算符}大小比较
bool string::operator<(const string& s)const{returnstrcmp(_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,并能在需要时定制自己的字符串类。
相关阅读
关注博主,第一时间获取更新!