
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[一、0基础手撕:从0搭建 string 核心底层逻辑(附实现代码)](#一、0基础手撕:从0搭建 string 核心底层逻辑(附实现代码))
[1、字符串获取:find 和 substr 的实现](#1、字符串获取:find 和 substr 的实现)
[1.1 字符串查找:find 的实现(找字符/子串)](#1.1 字符串查找:find 的实现(找字符/子串))
[1.1.1 查找单个字符:find(char)的实现](#1.1.1 查找单个字符:find(char)的实现)
[1.1.2 查找子串:find(const char*)的实现](#1.1.2 查找子串:find(const char*)的实现)
[1.2 截取子串:substr 的实现](#1.2 截取子串:substr 的实现)
[2、字符串的比较和输入输出:operator<<,operator>>,getline 与比较运算符的实现](#2、字符串的比较和输入输出:operator<<,operator>>,getline 与比较运算符的实现)
[2.1 字符串比较:operator==,operator< 等一系列比较运算符的实现](#2.1 字符串比较:operator==,operator< 等一系列比较运算符的实现)
[2.2 输入输出:operator<< 和 operator>> 与 getline 的实现](#2.2 输入输出:operator<< 和 operator>> 与 getline 的实现)
一、0基础手撕:从0搭建 string 核心底层逻辑(附实现代码)
1、字符串获取:find 和 substr 的实现
1.1 字符串查找:find 的实现(找字符/子串)
1.1.1 查找单个字符:find(char)的实现
查找单个字符时,从指定起始位置遍历字符串,逐个比对字符,找到则返回位置,遍历结束仍未找到则返回npos。
cpp
//string.h
public:
//字符串获取函数声明
size_t find(char ch, size_t pos = 0); //字符串查找:find(查找单个字符)
private:
static const size_t npos;
//string.cpp
namespace Mystring
{
const size_t string::npos = -1;
//之所以npos的定义放到.cpp文件而不是直接放到.h文件,
//是因为当.h被多个文件包含时,.h文件被展开时npos就被多次定义而导致报错
//字符串查找:find(查找单个字符)
size_t string::find(char ch, size_t pos)
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
void Test_string3()
{
string s1("Hello world");
cout << s1.find('w') << endl;
cout << s1.find('w', 8) << endl;
}
}

1.1.2 查找子串:find(const char*)的实现
在模拟实现查找子串之前,我们先了解一个算法(strstr):

它可以直接获取到查找子串首次出现位置的指针,非常方便。
cpp
//string.h
public:
//字符串获取函数声明
size_t find(const char* str, size_t pos = 0); //字符串查找:find(查找子串)
private:
static const size_t npos;
//string.cpp
namespace Mystring
{
const size_t string::npos = -1;
//之所以npos的定义放到.cpp文件而不是直接放到.h文件,
//是因为当.h被多个文件包含时,.h文件被展开时npos就被多次定义而导致报错
//字符串查找:find(查找子串)
size_t string::find(const char* str, size_t pos)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
//const char * strstr ( const char * str1, const char * str2 );
//返回指向str1中str2首次出现位置的指针,若str2不属于str1则返回空指针
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str; //由于strstr返回的是指针,所以用两个指针相减就能得到下标
}
}
void Test_string3()
{
string s1("Hello world");
cout << s1.find("orl") << endl;
cout << s1.find("hello") << endl;
cout << s1.find("orl", 8) << endl;
}
}

1.2 截取子串:substr 的实现
substr 用于指定位置截取连续n个字符,返回一个新的 string 对象,底层拷贝目标子串到新内存。
cpp
//string.h
public:
//拷贝构造(深拷贝)
string(const string& rstr)
{
_str = new char[rstr._size + 1];
_size = rstr._size;
_capacity = rstr._capacity;
memcpy(_str, rstr._str, rstr._size + 1);
}
//字符串获取函数声明
string substr(size_t pos, size_t len = npos); //截取子串:substr
private:
static const size_t npos;
//string.cpp
namespace Mystring
{
const size_t string::npos = -1;
//之所以npos的定义放到.cpp文件而不是直接放到.h文件,
//是因为当.h被多个文件包含时,.h文件被展开时npos就被多次定义而导致报错
//截取子串:substr
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
//如果len超过了pos位置后面剩余字符的长度,则需要更新len,使下面的循环不会越界
if (len > _size - pos)
{
len = _size - pos;
}
string sub; //创建一个空串来获取子串
sub.reserve(len); //提前扩容预留空间,避免后续额外扩容
for (size_t i = pos; i < pos + len; i++)
{
sub += _str[i];
}
return sub;
//学习了前面知识应该就知道这里是传值返回需要调用拷贝构造
//但由于string里面有动态开辟空间的成员变量,所以需要深拷贝
//单编译器默认生成的拷贝构造是不够用的,当出了函数后对象的_str就是野指针
}
void Test_string3()
{
string s1("Hello world");
string sub1 = s1.substr(5);
cout << sub1.c_str() << endl;
string sub2 = s1.substr(2, 5);
cout << sub2.c_str() << endl;
}
}

2、字符串的比较和输入输出:operator<<,operator>>,getline 与比较运算符的实现
字符串的比较(大小关系、相等性)和输入输出是基础且高频的操作。通过重载比较运算符,实现字符串大小判断,结合流插入 / 提取运算符,可让自定义的 string 类完全适配 C++ 的操作习惯。
2.1 字符串比较:operator==,operator< 等一系列比较运算符的实现
cpp
//string.h
//字符串比较函数声明(类外,让第一个形参不是隐含的this指针,可以实现常量字符串与string的比较)
bool operator>(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator<(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);
//string.cpp
namespace Mystring
{
//字符串比较函数声明(类外,让第一个形参不是隐含的this指针,可以实现常量字符串与string的比较)
bool operator>(const string& s1, const string& s2)
{
//利用C语言的strcmp函数即可
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 strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
void Test_string4()
{
string s1("Hello world");
string s2("Hello");
string s3("Hello world");
cout << (s1 == s2) << endl;
cout << (s1 == s3) << endl;
cout << (s1 > s2) << endl;
}
}

2.2 输入输出:operator<< 和 operator>> 与 getline 的实现
我们前面都是使用 c_str 进行打印观察 string 的,这里我们可以实现一下流插入和流提取。
cpp
//string.h
public:
void clear()
{
_str[0] = '\0';
_size = 0;
//clear函数只清数据不会释放空间
}
//流输入流输出函数声明(类外)
ostream& operator<<(ostream& out, string s);
istream& operator>>(istream& in, string& s);
//string.cpp
namespace Mystring
{
//流输入流输出
ostream& operator<<(ostream& out,string s)
{
//直接用范围for遍历将s的所有字符挨个写入
for (auto ch : s)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();//先清空原有内容
char ch;
//in >> ch;
//这个写法在模拟实现流输入时是有问题的
//因为 cin 会将空格和换行符当成分隔符而忽略掉
//当我们模拟实现流输入这样写的话 ch 就永远不可能读取到空格和换行符了,也就是说不会跳出循环
//所以我们需要借助get()
ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
//in >> ch;
ch = in.get();
//逻辑是:先输入一个字符传给ch,然后进入循环
//将ch写入s中,再输入一个字符传给ch以此循环
//当输入为空格或换行时则跳出循环
}
return in;
}
void Test_string4()
{
string s1("Hello world");
string s2("Hello");
string s3("Hello world");
cout << s1 << " " << s2 << endl;
cin >> s1 >> s2;
cout << s1 << " " << s2 << endl;
}
}

getline 的模拟实现和 operator>> 基本没有区别,只是判断循环终止的条件发生了改变:
cpp
//string.h
public:
void clear()
{
_str[0] = '\0';
_size = 0;
//clear函数只清数据不会释放空间
}
//流输入流输出函数声明(类外)
istream& getline(istream& in, string& s, char delim = '\n');
//string.cpp
namespace Mystring
{
istream& getline(istream& in, string& s, char delim)
{
s.clear();
char ch;
ch = in.get();
while (ch != delim)//不传第三个参数则默认为换行符
{
s += ch;
ch = in.get();
}
return in;
}
void Test_string4()
{
string s1("Hello world");
string s2("Hello");
string s3("Hello world");
getline(cin, s1);
cout << s1 << endl;
getline(cin, s2, '#');
cout << s2 << endl;
}
}

说明:
- 比较运算符完全遵循字典序,结果符合预期;
- operator>> 正确读取两个单词(以空格为分隔),getline 正确读取包含空格的整行内容;
结束语
到此,string 类基本功能的模拟实现就讲解完了。当你手撕 string 类后就会发现,它的本质无非就是 "动态数组 + 内存管理",如果前面的知识学的较为扎实这些实现其实也没有自己认为的那么难。扩容倍数、深拷贝等设计,都是效率与安全的权衡。在后续我也会分享更多容器模拟实现。"懂原理" 才是底气。希望这篇文章对大家学习C++能有所帮助!
C++参考文档:
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/