
🎬 博主名称 :键盘敲碎了雾霭
🔥 个人专栏 : 《C语言》《数据结构》 《C++》
⛺️指尖敲代码,雾霭皆可破

文章目录
- 一、前言:为什么要手写string类?
- 二、整体架构设计
-
- [2.1 成员变量设计](#2.1 成员变量设计)
- [2.2 类结构总览](#2.2 类结构总览)
- 三、核心构造与析构
-
- [3.1 构造函数](#3.1 构造函数)
- [3.2 析构函数](#3.2 析构函数)
- 四、深浅拷贝与拷贝控制
-
- [4.1 拷贝构造函数(现代写法)](#4.1 拷贝构造函数(现代写法))
- [4.2 赋值运算符重载(现代写法)](#4.2 赋值运算符重载(现代写法))
- [4.3 swap成员函数](#4.3 swap成员函数)
- 五、迭代器实现
-
- [5.1 普通迭代器](#5.1 普通迭代器)
- [5.2 const迭代器](#5.2 const迭代器)
- 六、容量管理
-
- [6.1 reserve扩容函数](#6.1 reserve扩容函数)
- [6.2 扩容策略分析](#6.2 扩容策略分析)
- 七、修改操作
-
- [7.1 push_back尾插字符](#7.1 push_back尾插字符)
- [7.2 append追加字符串](#7.2 append追加字符串)
- [7.3 operator+=重载](#7.3 operator+=重载)
- [7.4 insert插入操作](#7.4 insert插入操作)
- 八、查找与截取
-
- [8.1 find查找字符/子串](#8.1 find查找字符/子串)
- [8.2 substr截取子串](#8.2 substr截取子串)
- 九、运算符重载
-
- [9.1 比较运算符](#9.1 比较运算符)
- [9.2 流插入运算符<<](#9.2 流插入运算符<<)
- [9.3 流提取运算符>>](#9.3 流提取运算符>>)
- 文章结语
一、前言:为什么要手写string类?
string 相关接口应用 放在了上几篇文章中,不熟悉的朋友可以先点击下方链接快速回顾一下:
【C++初阶】string类(一):从基础到实战
【C++初阶】string类(二):常用接口全解析
手写一个简化版string类,是深入理解C++核心机制的最佳实践。通过这个项目,你将掌握:
| 核心知识点 | 应用场景 |
|---|---|
| 深拷贝与浅拷贝 | 自定义类的拷贝控制 |
| RAII原则 | 资源管理 |
| 迭代器模式 | 容器设计 |
| 运算符重载 | 类的接口设计 |
二、整体架构设计
2.1 成员变量设计
cpp
private:
char* _arr = nullptr; // 指向动态分配的字符数组
size_t _size = 0; // 字符串当前长度(不含'\0')
size_t _capacity = 0; // 实际分配的容量
static const size_t npos; // 静态常量,表示无效位置
2.2 类结构总览
cpp
namespace A {
class string {
public:
// 类型定义
typedef char* iterator;
typedef const char* const_iterator;
// 构造/析构
string(const char* p = "");
string(const string& s);
~string();
// 赋值
string& operator=(string tmp);
// 迭代器
iterator begin();
iterator end();
const_iterator begin() const;
const_iterator end() const;
// 容量
size_t size() const;
size_t capacity() const;
void reverse(size_t capacity); // 注意:应为reserve
// 修改
void push_back(char ch);
string& append(const char* str);
string& operator+=(char ch);
string& operator+=(const char* str);
string& insert(size_t pos, char ch);
string& insert(size_t pos, const char* str);
// 查找/截取
size_t find(char ch, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);
string substr(size_t pos = 0, size_t len = npos);
// 元素访问
char& operator[](size_t pos);
const char& operator[](size_t pos) const;
const char* c_str() const;
// 交换
void swap(string& s);
private:
char* _arr;
size_t _size;
size_t _capacity;
static const size_t npos;
};
}
三、核心构造与析构
3.1 构造函数
cpp
string(const char* p = "") {
_size = strlen(p);
_capacity = _size;
_arr = new char[_size + 1]; // +1 给'\0'
strcpy(_arr, p);
}
关键点:
- 默认参数
""确保空字符串也能正确处理 strlen不计算结尾的\0,所以分配_size + 1- 使用
strcpy拷贝包括\0在内的完整字符串
3.2 析构函数
cpp
~string() {
delete[] _arr;
_arr = nullptr;
_size = _capacity; // ⚠️ 这里赋值有误,应为 _capacity = 0
}
注意: 析构函数中_size = _capacity是笔误,应该释放资源后将成员清零:
cpp
~string() {
delete[] _arr;
_arr = nullptr;
_size = 0;
_capacity = 0;
}
四、深浅拷贝与拷贝控制
4.1 拷贝构造函数(现代写法)
cpp
// 传统写法
string(const string& s) {
_arr = new char[s.size() + 1];
strcpy(_arr, s._arr);
_size = s.size();
_capacity = s.capacity();
}
// 现代写法(推荐)
string(const string& s) {
string tmp(s._arr); // 复用构造函数
swap(tmp); // 交换资源
}
现代写法的优势:
- 避免代码重复
- 异常安全:如果
new失败,tmp构造失败,原对象不受影响 - 利用
swap实现高效的资源转移
4.2 赋值运算符重载(现代写法)
cpp
// 传值参数实现拷贝 + swap
string& operator=(string tmp) {
swap(tmp);
return *this;
}
这是C++中经典的"copy and swap"惯用法:
- 参数传值自动完成拷贝(深拷贝)
- 与
tmp交换资源 - 函数结束时
tmp销毁,带走原资源
4.3 swap成员函数
cpp
void swap(string& s) {
std::swap(_arr, s._arr);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
五、迭代器实现
5.1 普通迭代器
cpp
typedef char* iterator;
iterator begin() {
return _arr; // 指向第一个字符
}
iterator end() {
return _arr + _size; // 指向'\0'位置
}
为什么end()指向\0?
- 符合STL的"左闭右开"区间
[begin, end) - 遍历时
it != end(),不会访问\0
5.2 const迭代器
cpp
typedef const char* const_iterator;
const_iterator begin() const {
return _arr;
}
const_iterator end() const {
return _arr + _size;
}
const迭代器的作用:
- 允许const对象遍历
- 防止通过迭代器修改字符
六、容量管理
6.1 reserve扩容函数
cpp
void reverse(size_t capacity) { // 注意:应为 reserve
if (capacity > _capacity) {
char* tmp = new char[capacity + 1];
strcpy(tmp, _arr);
delete[] _arr; // ⚠️ 这里有问题
_arr = tmp;
_capacity = capacity;
}
}
函数名问题: 应该是reserve(预留),不是reverse(反转)。
隐藏的bug:
cpp
delete _arr; // 错误!应该用 delete[] _arr
delete[] _arr; // 正确
因为_arr是通过new char[]分配的,必须用delete[]释放。
6.2 扩容策略分析
| 当前容量 | 扩容后容量 |
|---|---|
| 0 | 4 |
| 4 | 8 |
| 8 | 16 |
| ... | 2倍增长 |
为什么选择2倍扩容?
- 均摊时间复杂度为O(1)
- 避免频繁申请内存
- 空间利用率与时间效率的平衡
七、修改操作
7.1 push_back尾插字符
cpp
void push_back(char ch) {
if (_size == _capacity) {
reverse(_capacity == 0 ? 4 : 2 * _capacity);
}
_arr[_size++] = ch;
_arr[_size] = '\0'; // 维护结尾的'\0'
}
注意: 每次修改后都要确保_arr[_size] = '\0'。
7.2 append追加字符串
cpp
string& append(const char* str) {
size_t len = strlen(str);
if (len + _size > _capacity) {
reverse(len + _size > 2 * _capacity ?
len + _size : 2 * _capacity);
}
strcpy(_arr + _size, str);
_size += len;
return *this;
}
7.3 operator+=重载
cpp
string& operator+=(char ch) {
push_back(ch);
return *this;
}
string& operator+=(const char* str) {
append(str);
return *this;
}
7.4 insert插入操作
插入单个字符:
cpp
string& insert(size_t pos, char ch) {
assert(pos <= _size);
if (_size == _capacity) {
reverse(_capacity == 0 ? 4 : 2 * _capacity);
}
size_t end = _size + 1;
while (end > pos) {
_arr[end] = _arr[end - 1];
end--;
}
_arr[pos] = ch;
_size++;
return *this;
}
插入字符串:
cpp
string& insert(size_t pos, const char* str) {
assert(pos <= _size);
size_t len = strlen(str);
if (len + _size > _capacity) {
reverse(len + _size > 2 * _capacity ?
len + _size : 2 * _capacity);
}
size_t end = _size + len;
while (end > pos + len - 1) {
_arr[end] = _arr[end - len];
end--;
}
for (size_t i = 0; i < len; i++) {
_arr[pos + i] = str[i];
}
_size += len;
return *this;
}
八、查找与截取
8.1 find查找字符/子串
查找字符:
cpp
size_t find(char ch, size_t pos = 0) {
assert(pos < _size);
for (size_t i = pos; i < _size; i++) {
if (_arr[i] == ch) return i;
}
return npos;
}
查找子串(利用C库函数):
cpp
size_t find(const char* str, size_t pos = 0) {
assert(pos < _size);
char* tmp = strstr(_arr + pos, str);
if (tmp == nullptr) return npos;
return tmp - _arr; // 指针相减得到下标
}
8.2 substr截取子串
cpp
string substr(size_t pos = 0, size_t len = npos) {
assert(pos < _size);
string sub;
if (len > _size - pos) {
len = _size - pos;
}
for (size_t i = 0; i < len; i++) {
sub += _arr[pos + i];
}
return sub;
}
九、运算符重载
9.1 比较运算符
cpp
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);
}
设计技巧: 只实现>和==,其他运算符通过它们推导,减少重复代码。
9.2 流插入运算符<<
cpp
ostream& operator<<(ostream& out, const string& s) {
for (auto ch : s) { // 范围for需要迭代器支持
cout << ch;
}
return out;
}
9.3 流提取运算符>>
cpp
istream& operator>>(istream& in, string& s) {
char ch = in.get();
size_t i = 0;
const size_t N = 256;
char buff[N];
while (ch != ' ' && ch != '\n') {
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;
}
缓冲区优化: 使用256字节的栈缓冲区,减少频繁的append调用。
📌 本文代码已上传至gittee,欢迎Star和PR!
如有错误或补充,欢迎在评论区留言讨论!
文章结语
感谢你读到这里~我是「键盘敲碎了雾霭」,愿这篇文字帮你敲开了技术里的小迷雾 💻
如果内容对你有一点点帮助,不妨给个暖心三连吧👇
👍 点赞 | ❤️ 收藏 | ⭐ 关注
(听说三连的小伙伴,代码一次编译过,bug绕着走~)
你的支持,就是我继续敲碎技术雾霭的最大动力 🚀
🐶 小彩蛋:
/^ ^\
/ 0 0 \
V\ Y /V
/ - \
/ |
V__) ||
摸一摸毛茸茸的小狗,赶走所有疲惫和bug~我们下篇见 ✨