跳到主要内容
C++ STL string 类从零实现详解 | 极客日志
C++ 算法
C++ STL string 类从零实现详解 本文详细解析了 C++ STL string 类的底层实现原理。涵盖内存管理策略、深浅拷贝机制、运算符重载及迭代器设计。重点阐述了构造函数、析构函数、赋值运算符的现代写法(copy-and-swap),以及容量扩容、插入删除等核心功能的代码逻辑。通过从零构建 string 类,深入理解 C++ 对象生命周期与资源控制。
前言
在前面的学习中,我们已经初步掌握了 string 类接口函数的使用方法。本文将带领大家从零开始,逐步实现一个完整的 string 类,深入理解其底层内存管理与核心逻辑。
一、string 类总览
为了避免与标准库中的 string 产生命名冲突,我们使用 mystd 命名空间进行封装。
namespace mystd {
class string {
public :
typedef char * iterator;
typedef const char * const_iterator;
string ();
string (const char * str);
string (const string& s);
string& operator =(const string& s);
~string ();
iterator begin () ;
iterator end () ;
const_iterator begin () const ;
const_iterator end () const ;
size_t size () ;
size_t capacity () ;
void reserve (size_t n) ;
;
;
;
;
string& +=( ch);
string& +=( * str);
;
;
;
;
;
;
& []( i);
& []( i) ;
;
;
;
;
:
* _str;
_size;
_capacity;
npos;
};
string::npos = ;
<( string& s1, string& s2);
<=( string& s1, string& s2);
>( string& s1, string& s2);
>=( string& s1, string& s2);
==( string& s1, string& s2);
!=( string& s1, string& s2);
ostream& <<(ostream& out, string& s);
istream& >>(istream& in, string& s);
}
void resize (size_t n, char ch = '\0' )
bool empty () const
void push_back (char ch)
void append (const char * str)
operator
char
operator
const
char
string& insert (size_t pos, char ch)
string& insert (size_t pos, const char * str)
string& erase (size_t pos, size_t len)
void clear ()
void swap (string& s)
const char * c_str () const
char
operator
size_t
const
char
operator
size_t
const
size_t find (char ch, size_t pos = 0 ) const
size_t find (const char * str, size_t pos = 0 ) const
size_t rfind (char ch, size_t pos = npos) const
size_t rfind (const char * str, size_t pos = 0 ) const
private
char
size_t
size_t
static
const
size_t
const
size_t
-1
bool
operator
const
const
bool
operator
const
const
bool
operator
const
const
bool
operator
const
const
bool
operator
const
const
bool
operator
const
const
operator
const
operator
char* _str:用来存储字符串。
size_t _size:记录当前字符串的长度(不包含'\0')。
size_t _capacity:记录字符串当前的容量(不包含'\0')。
static const size_t npos:这是一个静态成员变量,其值为 -1(即整型的最大值),通常用作函数的异常返回值。
typedef char* iterator:通过重命名 char* 指针作为迭代器。
typedef const char* const_iterator:通过重命名 const char* 指针作为常量迭代器。
二、默认成员函数
2.1 无参构造函数
string () : _str(new char [1 ]{'\0' }), _size(0 ), _capacity(0 ) {}
这里默认 string 类预留一个 '\0' 作为空字符的表示。例如 string s; 调用无参构造,表示 s 为空字符。初始化 _size 为 0,_capacity 为 0。注意空字符的字符个数 _size 和空间 _capacity 均为 0,即'\0'额外单独开空间。
2.2 带参构造函数
string (const char * str) {
_size = strlen (str);
_capacity = _size;
_str = new char [_capacity + 1 ];
strcpy (_str, str);
}
使用初始化列表进行构造时,初始化顺序是按照成员变量声明顺序执行的。为了复用 strlen 计算结果,不建议在此处使用初始化列表方式。
2.3 拷贝构造函数
浅拷贝 :拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间,其中一个对象的改动会对另一个对象造成影响。
深拷贝 :源对象与拷贝对象互相独立,其中任何一个对象的改动不会对另外一个对象造成影响。
string 类需要深拷贝的原因是每个 string 类对象需要独立的空间进行存储字符串,要求任何一个对象的改动不会对另外一个对象造成影响。深拷贝给每个对象独立分配资源,保证多个对象之间不会因共享资源而造成多次释放导致程序崩溃。
string (const string& s) : _size(s._size), _capacity(s._size)
{
_str = new char [_capacity + 1 ];
strcpy (_str, s._str);
}
首先为拷贝对象 str2 分配足够容纳源对象 str1 的内存空间,然后将源对象 str1 的字符串内容完整复制到 str2 中。由于 str2 的 _str 指针和 str1 的 _str 指针指向不同的内存空间,因此这是一个深拷贝操作,两个对象完全独立。
void swap (string& tmp) {
std::swap (_str, tmp._str);
std::swap (_size, tmp._size);
std::swap (_capacity, tmp._capacity);
}
string (const string& s) {
string tmp (s._str) ;
swap (tmp);
}
如果待拷贝对象 s2 的成员变量没有在类内给缺省值,也没有在拷贝构造函数的初始化列表中被初始化,在执行 swap(tmp, s2) 时,由于 s2 指向的是随机内存空间,交换后当 tmp 调用析构函数时会导致严重错误。
2.4 赋值运算符重载 赋值运算符重载与拷贝构造函数类似,同样需要考虑深浅拷贝问题。
string& operator =(const string& other) {
if (this == &other) { return *this ; }
delete [] _str;
_str = new char [other._capacity + 1 ];
strcpy (_str, other._str);
_size = other._size;
_capacity = other._capacity;
return *this ;
}
关键步骤包括防范自我赋值、清理旧资源防止内存泄漏、深拷贝新资源以及返回自身引用支持链式操作。
void swap (string& tmp) {
std::swap (_str, tmp._str);
std::swap (_size, tmp._size);
std::swap (_capacity, tmp._capacity);
}
string& operator =(string tmp) {
swap (tmp);
return *this ;
}
当执行 s1 = s2; 时,参数按值传递会自动调用拷贝构造函数克隆出临时对象 tmp。进入函数体后,执行 std::swap,直接把两者的底牌互换。此时 s1 拿到了 tmp 申请好的新内存和新数据,而 tmp 拿着 s1 淘汰下来的旧内存。随着函数结束,局部对象 tmp 的生命周期走到尽头,编译器自动调用它的析构函数,顺道就把 s1 之前的旧垃圾清理得干干净净。
2.5 析构函数 ~string () {
delete [] _str;
_size = 0 ;
_capacity = 0 ;
}
三、迭代器 string 类中的迭代器本质上是字符指针,iterator 只是其类型别名。
typedef char * iterator;
typedef const char * const_iterator;
3.1 begin iterator begin () { return _str; }
const_iterator begin () const { return _str; }
3.2 end 返回字符串中最后一个字符的后一个字符的地址(即'\0'的地址),即迭代器的末位置。
iterator end () { return _str + _size; }
const_iterator end () const { return _str + _size; }
四、容量和大小相关函数
4.1 size size_t size () const { return _size; }
4.2 capacity size_t capacity () const { return _capacity; }
4.3 reserve reserve 函数用于预留空间。当 n 超过对象当前 capacity 时,将 capacity 扩展至不小于 n 的大小;当 n 小于当前 capacity 时,维持现有 capacity 不变。
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.4 empty bool string::empty () {
return strcmp (_str, "" ) == 0 ;
}
五、字符串修改函数
5.1 push_back void string::push_back (char c) {
if (_size == _capacity) {
reserve (_capacity == 0 ? 4 : _capacity * 2 );
}
_str[_size++] = c;
_str[_size] = '\0' ;
}
尾插操作前检查容量是否足够,不足则扩容。尾插字符后必须在其后添加'\0'终止符,否则打印时可能出现非法访问。
5.2 append void string::append (const char * s) {
size_t len = strlen (s);
if (len + _size > _capacity) {
size_t newcapacity = (_size + len > _capacity * 2 ) ? _size + len : _capacity * 2 ;
reserve (newcapacity);
}
strcpy (_str + _size, s);
_size += len;
}
若空间不足需先扩容,完成后追加字符串。由于待插入字符串本身已包含终止符'\0',故无需额外添加。
5.3 operator+= string& string::operator +=(char c) {
this ->push_back (c);
return *this ;
}
string& string::operator +=(const char * s) {
this ->append (s);
return *this ;
}
对于字符串与字符的尾插,直接调用 push_back 函数即可实现。对于字符串与字符串的尾插,通过调用 append 函数进行实现。
5.4 insert void string::insert (size_t pos, char c) {
assert (pos <= _size);
if (_size == _capacity) {
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2 ;
reserve (newcapacity);
}
size_t end = _size + 1 ;
while (end > pos) {
_str[end] = _str[end - 1 ];
--end;
}
_str[pos] = c;
_size++;
}
化解 size_t 死循环危机:把 end 的初始值设为 _size + 1,并用 end > pos 作为条件。保证了即使在 pos = 0 的最极端情况下,end 减到 0 循环就会立刻停止,绝不会发生整型下溢导致的内存越界崩溃。
void string::insert (size_t pos, const char * s) {
assert (pos <= _size);
size_t len = strlen (s);
if (len == 0 ) return ;
if (len + _size > _capacity) {
size_t newcapacity = len + _size > _capacity * 2 ? len + _size : _capacity * 2 ;
reserve (newcapacity);
}
size_t end = _size + len;
while (end > pos + len - 1 ) {
_str[end] = _str[end - len];
--end;
}
for (size_t i = 0 ; i < len; i++) {
_str[pos + i] = s[i];
}
_size += len;
}
先搬家 '\0',依次搬家后续字符,直到腾出指定空间,最后把新的字符串 s 塞进这个空位里。
5.5 erase 删除字符串从 pos 位置开始的 len 个字符。
void string::erase (size_t pos, size_t len) {
assert (pos < _size);
if (len >= _size - pos) {
_str[pos] = '\0' ;
_size = pos;
} else {
size_t begin = pos + len;
while (begin <= _size) {
_str[begin - len] = _str[begin];
++begin;
}
_size -= len;
}
}
5.6 substr string string::substr (size_t pos, size_t len) {
assert (pos < _size);
if (len >= _size - pos) {
len = _size - pos;
}
string tmp;
tmp.reserve (len);
for (size_t i = 0 ; i < len; i++) {
tmp += _str[pos + i];
}
return tmp;
}
5.7 clear void string::clear () {
_str[0 ] = '\0' ;
_size = 0 ;
}
六、访问字符串相关函数
6.1 operator[] 使 string 对象能够像 C 字符串一样,通过下标访问特定位置的字符。
char & operator [](size_t pos) {
assert (pos < _size);
return _str[pos];
}
const char & operator [](size_t pos) const {
assert (pos < _size);
return _str[pos];
}
6.2 find size_t string::find (char c, size_t pos) {
assert (pos < _size);
for (size_t i = pos; i < _size; i++) {
if (_str[i] == c) {
return i;
}
}
return npos;
}
size_t string::find (const char * s, size_t pos) {
assert (pos < _size);
const char * ptr = strstr (_str, s);
if (ptr != nullptr ) {
return ptr - _str;
} else {
return npos;
}
}
七、比较运算符重载 C++ 提供了六个关系运算符。在实际应用中,我们只需为类重载其中两个运算符,其余四个可以通过已重载的运算符来实现。
bool operator <(const string& s1, const string& s2) {
return strcmp (s1. c_str (), s2. c_str ()) < 0 ;
}
bool operator ==(const string& s1, const string& s2) {
return strcmp (s1. c_str (), s2. c_str ()) == 0 ;
}
bool operator <=(const string& s1, const string& s2) {
return s1 < s2 || s1 == s2;
}
bool operator >(const string& s1, const string& s2) {
return !(s1 <= s2);
}
bool operator >=(const string& s1, const string& s2) {
return !(s1 < s2);
}
bool operator !=(const string& s1, const string& s2) {
return !(s1 == s2);
}
八、字符串输入与输出
8.1 >>运算符的重载 istream& operator >>(istream& in, string& s) {
s.clear ();
const int N = 256 ;
char buff[N] = { 0 };
int i = 0 ;
char ch = in.get ();
while (ch == ' ' || ch == '\n' || ch == '\t' ) {
ch = in.get ();
}
while (ch != ' ' && ch != '\n' && ch != '\t' ) {
buff[i++] = ch;
if (i == N - 1 ) {
buff[i] = '\0' ;
s += buff;
i = 0 ;
}
ch = in.get ();
}
if (i > 0 ) {
buff[i] = '\0' ;
s += buff;
}
return in;
}
重置状态确保覆盖写入,准备缓冲数组避免频繁扩容,跳过前导空白,遇到非空白字符开始装车,装满卸货,遇到空格换行停车,最后打扫战场。
8.2 <<运算符的重载 ostream& operator <<(ostream& out, const string& s) {
for (auto ch : s) {
out << ch;
}
return out;
}
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online