前言
C++ 标准库std::string是我们日常开发最常用的容器之一,但很多同学只会调用接口,不清楚它底层存储、迭代器、比较运算符的实现逻辑。 本文结合课堂笔记手绘原理图,从零拆解简易版string,搞懂三大核心底层逻辑:堆内存存储结构、迭代器本质、关系运算符重载 ,同时对比标准库basic_string模板设计。
一、std::string 底层模板原型
标准库的string并不是单独类,而是模板basic_string的实例化:
template < class charT,
class traits = char_traits<charT>,
class Alloc = allocator<charT>
> class basic_string;
// string 等价于 basic_string<char>
using string = basic_string<char>;
// wstring 等价于 basic_string<wchar_t>
using wstring = basic_string<wchar_t>;
配套同体系容器vector也是模板实现,两者底层都是连续堆内存数组 ,区别在于string专门处理字符序列,末尾自带\0兼容 C 语言const char*。
参考官方文档:string - C++ Reference
二、简易 string 类存储结构(手绘原理图解析)
1. 成员变量定义
我们自定义daju::string只保留三大核心成员,和标准库底层设计对齐:
namespace daju
{
class string
{
private:
size_t _size; // 有效字符个数,不含末尾'\0'
size_t _capacity; // 堆内存总容量
char* _str; // 指向堆上字符数组
};
}
2. 内存分配原理图解读
-
字面量
"hello world\0"存放在只读常量区; -
构造函数内通过
new char[]在堆开辟内存,把常量区字符串拷贝到堆; -
成员指针
_str指向堆内存首地址; -
_size记录有效字符长度,_capacity记录堆数组总容量,堆数组末尾强制补\0,保证c_str()能返回兼容 C 的字符串。
3. 带参构造函数实现
string::string(const char* str)
:_size(strlen(str))
{
_capacity = _size;
// 多开1字节存放'\0'
_str = new char[_size + 1];
// 拷贝常量区字符串到堆
strcpy(_str, str);
}
无参构造单独分配 1 字节内存存放空终止符:
string::string()
:_str(new char[1] {'\0'})
,_size(0)
,_capacity(0)
{}
4. 兼容 C 接口 c_str ()
对外返回堆字符数组首地址,兼容所有接收const char*的 C 函数:
const char* string::c_str() const
{
return _str;
}
必须加
const修饰函数:常对象也能调用该接口,符合标准库设计规范。
三、string 迭代器底层本质(极简实现)
很多人以为迭代器是复杂类,对于string来说,迭代器本质就是 char \ 指针*。
1. 迭代器核心代码
// 迭代器重定义为char*
typedef char* iterator;
// begin:返回首字符地址
iterator begin()
{
return _str;
}
// end:返回有效字符末尾下一个位置
iterator end()
{
return _str + _size;
}
2. 两种遍历方式原理
方式 1:范围 for 循环(语法糖,底层自动调用 begin/end)
daju::string s2("hello");
for (auto ch : s2)
{
cout << ch << " ";
}
方式 2:原生迭代器循环,直观体现指针操作
daju::string::iterator it1 = s1.begin();
while (it1 != s1.end())
{
cout << *it1 << " ";
++it1;
}
原理图逻辑: begin()指向_str[0],end()指向_str[_size](末尾\0前一位),循环条件it != end()避免越界。
四、关系运算符重载 ==、>、< 底层逻辑
1. 标准库运算符重载分类
标准库对string关系运算符分 6 组重载,覆盖 4 种匹配场景:
-
stringvsstring -
stringvsconst char* -
const char*vsstring -
const char*vsconst char*
2. 核心比较规则(重点)
字符串比较不是比较长度,是逐字符 ASCII 码对比 ,逻辑和 C 库strcmp完全一致:
-
从第 0 位开始,依次对比两个字符串对应字符 ASCII;
-
第一个不相等的字符,ASCII 大的字符串整体更大;
-
若短字符串是长字符串前缀,则更长的字符串更大。
示例:
string s1("xxx");
string s2("xxx11");
const char* str = "23222";
s1 == s2; // false
s1 == str; // false
str = s1; // 赋值,非比较
3. 关键设计细节:== 运算符不能只做成员重载
如果只把operator==写成类内成员函数,只能支持string == const char*,无法实现const char* == string; 所以标准库将所有关系运算符全局重载,同时提供两套参数版本,覆盖正反比较场景。
五、当前简易版遗留缺陷(拓展优化方向)
我们当前只实现基础框架,生产级string还需要补充这些功能:
- 析构函数 :
new[]必须配套delete[]释放堆内存,否则内存泄漏
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
-
深拷贝拷贝构造 + 赋值重载 默认浅拷贝会让两个对象共用同一块堆内存,析构时重复释放触发崩溃,必须手动开辟新堆空间拷贝字符。
-
扩容接口 reserve /resize 当前构造
capacity和size相等,不支持尾插字符;标准库会预分配冗余容量减少频繁new/delete开销。 -
增删接口:
push_back、operator+=、erase、substr等
六、总结
-
std::string底层基于basic_string<char>模板,堆内存连续数组存储字符,末尾带\0兼容 C 字符串; -
简易 string 迭代器本质就是原生
char*,begin()取首地址,end()取有效字符后一位; -
关系运算符重载逻辑依托
strcmpASCII 逐位比较,和字符串长度无关; -
手写简易 string 能彻底理解容器内存管理、迭代器、运算符重载三大 C++ 核心知识点,为后续学习
vector打下基础。