一、为什么要模拟实现 string?
在 C++ 中,std::string 是 STL 中最常用的容器之一。
平时我们写:
std::string s = "hello";
实际上背后进行了很多工作:
-
动态内存申请
-
字符串拷贝
-
深拷贝管理
-
构造和析构
-
运算符重载
-
迭代器支持
-
越界检查
如果不了解这些底层机制,就很难真正理解 STL。
因此,通过手写一个简易版 string,可以深入理解:
-
类的资源管理
-
动态内存
-
深浅拷贝
-
RAII思想
-
运算符重载
-
Rule of Three / Rule of Five
二、string 的底层结构
一个简单的 string,本质上就是:
一块动态开辟的字符数组 + 当前字符串长度
例如:
string s = "hello";
内存:
+---+---+---+---+---+----+
| h | e | l | l | o | \0 |
+---+---+---+---+---+----+
len = 5
capacity = 5
成员变量:
char* _str; // 存储字符串
size_t _size; // 字符数量
size_t _capacity; // 容量
三、基本框架设计
首先创建一个自己的字符串类:
#include <iostream>
#include <cstring>
using namespace std;
class String
{
private:
char* _str;
size_t _size;
size_t _capacity;
};
四、构造函数
1. 默认构造
创建一个空字符串:
String()
{
_str = new char[1];
_str[0] = '\0';
_size = 0;
_capacity = 0;
}
效果:
_str
+----+
| \0 |
+----+
size = 0
2. 字符串构造
例如:
String s("hello");
实现:
String(const char* str)
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
为什么:
_capacity + 1
?
因为 C 风格字符串必须包含:
'\0'
结束标志。
五、析构函数
因为我们自己申请了空间:
new char[]
所以必须释放:
~String()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
否则会造成:
内存泄漏
六、深拷贝问题
浅拷贝
例如:
String s1("hello");
String s2 = s1;
如果直接复制:
s1
|
v
+---------+
| hello |
+---------+
s2
|
v
+---------+
| hello |
+---------+
两个对象指向同一块空间。
析构时:
delete[]
会释放两次。
导致:
double free
七、拷贝构造函数
需要重新申请空间:
String(const String& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
这叫:
深拷贝
八、赋值运算符重载
例如:
s2 = s1;
需要:
String& operator=(const String& s)
{
if(this != &s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
为什么:
return *this;
?
支持连续赋值:
a = b = c;
九、获取字符串长度
实现:
size_t size() const
{
return _size;
}
使用:
String s("hello");
cout << s.size();
输出:
5
十、operator\[\] 下标访问
我们希望:
s[0]
访问字符。
实现:
char& operator[](size_t pos)
{
return _str[pos];
}
使用:
String s("hello");
s[0] = 'H';
cout << s;
输出:
Hello
十一、输出运算符重载
让:
cout << s;
成立。
实现:
ostream& operator<<(ostream& out,const String& s)
{
out << s._str;
return out;
}
十二、扩容机制
string 最大的问题:
空间不够怎么办?
例如:
String s("hello");
s += " world";
容量不足。
需要扩容:
void reserve(size_t capacity)
{
if(capacity > _capacity)
{
char* tmp = new char[capacity + 1];
strcpy(tmp,_str);
delete[] _str;
_str = tmp;
_capacity = capacity;
}
}
十三、追加字符串 operator+=
实现:
String& operator+=(const char* str)
{
size_t len = strlen(str);
if(_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size,str);
_size += len;
return *this;
}
使用:
String s("hello");
s += " world";
cout << s;
输出:
hello world
十四、完整简易版 String
class String
{
private:
char* _str;
size_t _size;
size_t _capacity;
public:
String()
{
_str = new char[1];
_str[0]='\0';
_size=0;
_capacity=0;
}
String(const char* str)
{
_size=strlen(str);
_capacity=_size;
_str=new char[_capacity+1];
strcpy(_str,str);
}
~String()
{
delete[] _str;
}
String(const String& s)
{
_size=s._size;
_capacity=s._capacity;
_str=new char[_capacity+1];
strcpy(_str,s._str);
}
String& operator=(const String& s)
{
if(this!=&s)
{
char* tmp=new char[s._capacity+1];
strcpy(tmp,s._str);
delete[] _str;
_str=tmp;
_size=s._size;
_capacity=s._capacity;
}
return *this;
}
size_t size()const
{
return _size;
}
char& operator[](size_t pos)
{
return _str[pos];
}
};
十五、模拟实现 string 学到了什么?
通过实现 string,我们掌握了:
1. 动态内存管理
new
delete
2. 深浅拷贝
理解:
-
浅拷贝的问题
-
深拷贝解决方案
3. RAII思想
对象生命周期管理资源:
构造申请资源,析构释放资源。
4. 运算符重载
包括:
=
[]
<<
+=
5. STL 容器设计思想
真正的 STL 容器,本质也是:
数据管理
+
资源管理
+
接口封装
十六、总结
手写 string 虽然代码量不大,但是涵盖了 C++ 面向对象编程中最核心的知识:
-
类和对象
-
构造函数
-
析构函数
-
拷贝构造
-
运算符重载
-
动态内存
-
深浅拷贝
-
RAII
理解了 string 的实现,再学习:
-
vector
-
list
-
map
-
unordered_map
会更加容易。
因为 STL 容器的核心思想都是:
管理资源,并提供安全、高效、易用的接口。