【C++指南】string(三):basic_string底层原理与模拟实现详解

【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);// 交换容量}

七、性能优化与注意事项

  1. substr的优化
    • 避免直接使用newstrcpy,通过reserve预分配内存减少扩容次数。
    • 若需要高性能,可实现“浅拷贝+引用计数”(需处理写时复制逻辑)。
  2. find的局限性
    • 当前实现为暴力匹配,标准库可能使用更高效的算法(如KMP)。
  3. 流提取的安全性
    • 缓冲区大小固定为256,若输入过长可能丢失数据,可动态调整缓冲区大小。
  4. swap的优势
    • 仅交换指针和元数据,时间复杂度为O(1),适合频繁交换场景。

结语

通过手写string类,我们深入理解了basic_string的底层机制。标准库的实现在此基础上进行了大量优化(如SSO、内存池),但核心逻辑与本文的模拟实现高度一致。掌握这些原理后,读者可以更高效地使用std::string,并能在需要时定制自己的字符串类。

相关阅读

关注博主,第一时间获取更新!

Read more

【C++笔记】STL详解:vector容器的使用

【C++笔记】STL详解:vector容器的使用

前言:         本文在介绍STL框架基础上,进一步讲解了迭代器、auto关键字和范围for循环的使用方法,接下来我们将重点探讨vector类的常用接口及其应用。          一、vector容器的简介             C++ 的 vector 是标准模板库(STL)中最核心且实用的容器之一,其与固定大小的传统数组(如 int arr[10])不同,vector 克服了数组的局限性,它不需要预先确定大小,并且可以动态调整容量。          简单理解为:vector是可变的、经过封装函数功能的数组。                  核心优势:          ①动态扩容:您不需要一开始就告诉它要存多少数据。当空间不够时,它会在底层自动帮您寻找一块更大的内存,把数据搬过去。          ②内存安全:它负责自己内存的分配和释放,大大减少了手动 new 和 delete 带来的内存泄漏风险。          ③功能丰富:它自带了大量现成的工具函数,比如:获取大小、清空数据、在尾部添加数据等。

By Ne0inhk

NumCpp实战指南:从零开始掌握C++数值计算的利器

NumCpp实战指南:从零开始掌握C++数值计算的利器 【免费下载链接】NumCppC++ implementation of the Python Numpy library 项目地址: https://gitcode.com/gh_mirrors/nu/NumCpp NumCpp是一个C++实现的Python Numpy库,为C++开发者提供了强大的数值计算能力。无论是科学计算、数据分析还是工程应用,NumCpp都能帮助开发者轻松处理多维数组和矩阵运算,是C++数值计算的必备工具。 为什么选择NumCpp? 熟悉的Numpy风格API NumCpp采用了与Numpy相似的API设计,让熟悉Python Numpy的开发者能够快速上手。这意味着你可以使用类似的函数名称和参数结构,大大降低了学习成本。 高效的C++性能 作为C++库,NumCpp充分利用了C++的性能优势,比纯Python实现的Numpy在计算密集型任务上更快。这使得NumCpp成为处理大规模数据和复杂算法的理想选择。 丰富的功能模块 NumCpp提供了丰富的功能模块,包括线性代数、傅里叶变换、

By Ne0inhk
C++之模版详解(进阶)

C++之模版详解(进阶)

目录 1. 非类型模板参数 2. 类模板的特化 2.1 函数模板特化 2.2 类模版特化 3. 模板的分离编译 1. 非类型模板参数 模版参数有两种,一种叫类型模版参数,一种叫做非类型模版参数。今天我们来讲讲非类型模版参数。 template <int N> 中的 int N 就是典型的非类型模板参数。这里的 int 是参数的类型,而 N 是参数名,它接收的是一个具体的常量值,而非像普通类型模板参数(如 template <typename T>)那样接收一个 “类型”。 两者核心区别就是: * 类型模板参数:传递 “类型”(如 T

By Ne0inhk
【C++11】列表初始化、新式声明、范围for和STL中的变化

【C++11】列表初始化、新式声明、范围for和STL中的变化

C++11新特性 * C++11新特性 * github地址 * 0. 前言 * 1. C++与C++11简介 * C++的发展简史 * C++11的意义 * 小故事:C++11命名的由来 * 2. 统一的列表初始化 * C++98中传统的{}初始化 * C++11中统一的列表初始化 * 列表初始化 * std::initializer_list * 引入 * initializer_list介绍 * vector补充支持initializer_list的构造 * map相关 * 3. C++11的新声明 * 1. auto * 1. C++类型系统演进 * 1.1 从C到C++的类型困境 * 1.2 typedef的局限性

By Ne0inhk