【C++】模拟实现string类
🦄个人主页:修修修也
🎏所属专栏:C++
⚙️操作环境:Visual Studio 2022
编辑
目录
一.了解项目功能
在上篇博客中我们详细介绍了C++标准库string类型,包含它的常用成员函数及其使用示例:【C++】 标 准 库类 型 string https://blog.csdn.net/weixin_72357342/article/details/136852268?spm=1001.2014.3001.5502
而在本次项目中我们的目标是模拟实现一个string类:
该string包含四个成员变量,分别是:
• char*类型成员变量_str,用于存放指向字符串的指针.
• size_t类型成员变量_size,用于存放类对象中的字符数量(不包含末尾的'\0').
• size_t类型成员变量_capacity,用于存放类对象的字符容量.
• static size_t类型静态成员变量nops,用于标志字符串的末尾位置-1.
模拟实现的成员函数有:
1. 构造函数,拷贝构造函数,赋值运算符重载和析构函数
2. c_str()函数
3. size()函数
4. reserve()函数
5. resize()函数
6. push_back()函数
7. append()函数
8. insert()函数
9. erase()函数
10. find()函数
11. substr()函数
12. clear()函数
13. swap()函数
14. 运算符重载函数,包括: = , [] , += , < , == , <= , > , >= , != , << , >>
15. 迭代器相关函数,包括:begin()函数,end()函数
二.逐步实现项目功能模块及其逻辑详解
通过第一部分对项目功能的介绍,我们已经对string类的功能有了大致的了解,虽然看似需要实现的功能很多,貌似一时间不知该如何下手,但我们可以分步分模块来分析这个项目的流程,最后再将各部分进行整合,所以大家不用担心,跟着我一步一步分析吧!
!!!注意,该部分的代码只是为了详细介绍某一部分的项目实现逻辑,故可能会删减一些与该部分不相关的代码以便大家理解,需要查看或拷贝完整详细代码的朋友可以移步本文第三部分。
🎏构建成员变量
构建成员变量部分的逻辑比较简单,前面我们也分析过string类需要的4个成员变量,基础问题就不过多赘述了,代码如下:
//设置命名空间,防止与库中的string类冲突
namespace mfc
{
class string
{
public:
//成员函数
private:
//成员变量
char* _str;
size_t _size;
size_t _capacity;
static size_t npos;
};
size_t string::npos = -1;//静态成员变量只在类外初始化一次
};
🎏实现string类默认成员函数
一般的类默认成员函数有6个,分别是构造函数,析构函数,拷贝构造函数,赋值运算符重载函数,普通取地址重载函数,和const取地址重载函数:编辑
对于string类而言,六个默认成员函数我们只需要实现前4个默认成员函数即可,取地址重载函数不需要我们专门手动实现,因为系统自动生成的默认函数就完全可以满足我们的需求。
📌构造函数
注意:
• 初始化列表不是按照代码编写的顺序执行的,而是按照类成员变量声明时的顺序初始化的,因此如下默认构造函数的代码是有问题的:
•
//设置命名空间,防止与库中的string类冲突
• namespace mfc
• {
• class string
• {
• public:
• //默认构造函数
• string(const char* str)
• :_size(strlen(str))
• ,_capacity(_size)
• ,_str(new char[_capacity + 1])
• {}
•
• private:
• //成员变量
• char* _str;
• size_t _size;
• size_t _capacity;
•
• static size_t npos;
• };
• size_t string::npos = -1;//静态成员变量只在类外初始化一次
• };
编辑
• 在开完空间后,对于字符串的构造还需要将形参的内容拷贝到类对象成员中,对于string类型的无参构造,我们可以选择在传参部分给一个缺省值(即一个空字符串),这样就可以很好的解决这个问题,因此整合后的代码如下:
•
//构造
• string(const char* str = "")
• :_size(strlen(str))
• ,_capacity(_size)
• {
• //只是给_str开了空间
• _str = new char[_capacity + 1];//多开一个空间放'\0'
• //这步才是给_str放入数据
• memcpy(_str, str,_size+1);
}
📌析构函数
因为string类对象在构造时动态开辟了存储字符的空间,因此我们就需要手动在析构函数里完成对动态开辟空间的释放,故析构函数代码如下:
~string()
{
//判断_str不是空指针再释放
if (_str)
{
delete[] _str; //释放动态开辟空间_str
_str = nullptr;
_size = 0;
_capacity = 0;
}
}
📌拷贝构造函数
和我们之前实现的Date类不同,string类是一个典型的需要实现深拷贝的类(【C++】 详 解深浅拷 贝 的概念及其区 别 ),系统默认生成的浅拷贝不能满足我们的需求,因此我们需要自己手动实现深拷贝:
深拷贝的逻辑不难,共有三步:
1. 动态开辟内存空间
2. 拷贝原类对象动态开辟空间内容到新开辟的空间中
3. 拷贝原类对象的其他内置类型的成员变量
综上,实现深拷贝的拷贝构造函数代码如下:
//拷贝构造
string(const string& s)
{
_str = new char[s._capacity + 1];
memcpy(_str, s._str, _size + 1);
_size = s._size;
_capacity = s._capacity;
}
对于拷贝构造函数,如果我们想要利用swap()来实现更简便的写法,就要面临两个无法解决的问题:
//利用swap()函数拷贝构造
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s);//会无穷递归调用拷贝构造导致栈溢出
swap(tmp);
}
而如果我们解决了第一个问题,就会出现第二个问题:
//利用swap()函数拷贝构造
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
string tmp(s._str);//会拷不到\0后的内容
swap(tmp);
}
📌赋值运算符重载函数
首先,我们使用一个真实的场景来分析赋值运算符所要完成的功能,如:
string st1("Hello");
string st2("yyyyyxxxxx");
st1 = st2;
在还未赋值前,两个类对象的状态是下图这样的:编辑
而当我们想要把st2赋值给st1时,我们期望达到的效果是这样的:
编辑
和拷贝构造的逻辑类似,我们同样期望赋值操作实现的是"深赋值",即st1和st2赋值后都有各自独立的空间存储相同的内容.
基于这样的功能诉求,我们大概可以设计赋值操作符重载的传统函数逻辑了,即
1. 先开一段新的内存空间
2. 再拷贝内容到新的空间去
3. 释放原空间,指向新空间
4. 修改_size和_capacity
综上思路,代码如下:
//传统思路代码
string& operator=(const string& s)
{
if (this != &s)
{
//开辟新空间
char* tmp = new char[s._capacity + 1];
//拷贝内容到新空间
memcpy(tmp, s._str, s._size + 1);
//释放旧空间
delete[] _str;
//指向新空间
_str = tmp;
//调整_size和_capacity
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
但是,我们下面还要介绍一种更为先进的思路,先上代码给大家体验一下:
//先进思路写法
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s);
swap(tmp);
}
return *this;
}
这个思路是先使用s拷贝构造一个局部临时变量tmp,再将tmp的内容和this的内容做交换,这样交换后的this的内容就是我们想要得到的s赋值后的内容了,并且由于类对象出了作用域自动销毁,因此我们也不需要再手动销毁交换后的tmp了,因为编译器会自动帮助我们处理掉,该思路图示如下:
编辑
编辑
编辑
上面的代码似乎足够简洁并且无可挑剔了,但仔细观察一下,其实还有可以优化的点,如:
//最终优化版
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
这段代码利用了形参是实参的一份临时拷贝这个特点,巧妙的将this指针和待赋值的参数的形参做交换,这样就可以简化代码少做一次局部变量的深拷贝构造和销毁,对比上面的代码无论是简洁度还是效率都又提高了不少.
🎏实现string类成员函数
📌c_str()函数
c_str()函数的作用是返回string类c语言形式的字符串,即string类对象中的_str成员,该函数逻辑较为简单,但还有一些细节需要注意:
首先,函数的返回值是需要拿const修饰的,这样函数返回的内容就不能够被修改.
其次,我们需要给参数列表的括号后面加上一个const,这个const是用来修饰形参部分的this指针的,而作用是为了便于const修饰的类对象也可以调用这个函数.因为权限可以缩小,但不能放大.
综上,代码如下:
const char* c_str() const//使const对象也可以调用
{
return _str;
}
📌size()函数
size()函数的作用是返回当前string对象中字符的个数,该函数逻辑较为简单,我们直接返回类对象中的_size成员即可,但也要注意给形参this指针加上const 修饰,以便于const对象也可以调用函数.
代码如下:
size_t size() const//使const对象也可以调用
{
return _size;
}
📌reserve()函数
reserve()函数的作用是接收一个无符号整型值n,然后修改string类对象的容量大小为n.
在实现reserve()函数时,我们首先要判断n是否大于当前类对象的容量,即判断这次reserve()函数的调用目的是"扩容"还是"缩容",因为调整容量的代价是需要重新开辟目标大小的空间并拷贝原本空间中的数据,会导致效率变低.相比于这个,未缩容导致的空间浪费几乎可以忽略,因此我们的实现策略是只在需要扩容时才调整容量大小,如果是缩容,则不做任何处理.
扩容实现逻辑如下:
1. 动态开辟比目标容量大一个字节(这个字节用于存放'\0')的空间.
2. 拷贝原空间内容到新开辟的空间.
3. 释放原空间.
4. 修改_str指针,使其指向新开辟的空间.
5. 修改容量_capacity的大小为n.
综上所述,reserve()函数实现代码如下:
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
memcpy(tmp, _str, _size + 1);
//这里使用strcpy拷贝的话就可能出现对于有\0的字符串的拷贝错误现象
delete[] _str;//一定要记得释放_str!!!!!!!
_str = tmp;
_capacity = n ;
}
}
📌push_back()函数
push_back()函数的作用是在字符串尾部插入一个字符ch,但在插入字符前,我们要先判断类对象的容量空间是否足够,只有容量够,才能进行尾插,否则要先执行扩容逻辑.扩容时我们只需要调用reserve()函数进行2倍扩容即可,但在reserve()函数参数部分,不能直接传入_capacity*2,因为如果当前字符串是一个空串,容量为0,则*2后还是0,会导致扩容失败.
当扩好容后,我们就可以直接在字符串的_size位置插入字符ch了,插入完成后,给_size++,并在字符串++后的_size位置放入一个'\0'字符作为终止标识符.
push_back()函数代码如下:
void push_back(char ch)
{
//查满扩容
if (_size == _capacity)
{
//2倍扩容
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
//尾插一个字符
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
📌append()函数
append()函数的作用是在string类对象后追加一个字符串.在追加字符串前,我们要先判断当前类对象的容量是否够用,即待插入的字符串的长度len是否大于类对象容量_capacity,如果小于,则要先将容量扩到_size+len,再将待插入的字符串拷贝到类对象字符串后面.如果大于,则可以直接将待插入的字符串拷贝到类对象字符串后面.
综上,append()函数实现代码如下:
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
//扩容,且至少到_size+len,不能是二倍扩容!
reserve(_size + len );//不+1,在reserve内部考虑
}
memcpy(_str + _size, str,len + 1);
_size += len;
}
📌insert()函数
insert()函数的作用是在string类对象字符串中插入内容.C++标准库中insert()函数实现了7个重载版本:编辑 有些过于冗余,我们这里只实现两种版本:
1. 往pos位置插入n个char.
2. 往pos位置插入一个字符串.
insert()函数的算法逻辑为:
• 判断pos位置是否合理,不合理需要抛出异常
• 判断容量是否够用,如果不够需要扩容
• 挪动后面的数据
• 插入数据到挪出来的位置上
• _size变为_size+n
在insert()函数的挪动数据过程中,有一点需要我们注意,就是如果end是size_t类型的,那么如果while循环只设定一个判断条件(end >= pos),那么就会出现以下问题:编辑
而如果我们在这里将end设置成int型,那么整型end与size_t型pos在比较时又会进行整型提升,我们可以编写一个程序测试一下整型提升:
如下程序可以看到,在内存中,整型的-1是比size_t型的0要大的:
编辑
对于以上问题,解决方式很多,我们可以选择在比较前就将pos强转为int型,也可以选则从后一个位置挪前一个的数据,思路如下图:编辑 还可以选择设置一个静态成员变量用来表示end已经走到末尾了的那个位置,即size_t型的"-1":
static size_t npos;
//静态成员变量在类外初始化
size_t string::npos = -1;
这样,我们就可以在循环的判定条件中加入&& end != npos这个判断条件,就可以有效防止上面出现的问题了.
//pos位置插入n个char
void insert(size_t pos, size_t n, char ch)
{
//判断pos是否合理
assert(pos <= _size);
//判断容量
if (_size + n > _capacity)
{
reserve(_size + n);
}
//挪动后面的数据
size_t end = _size;
//end和pos比较时会进行整型提升!
while (end >= pos && end != npos)
{
_str[end + n] = _str[end];
end--;
}
//插入数据
for (size_t i = 0; i < n; i++)
{
_str[pos + i] = ch;
}
_size += n;
}
//pos位置插入一个字符串
void insert(size_t pos,const char* str)
{
//判断pos是否合理
assert(pos <= _size);
//判断容量
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
//挪动后面的数据
size_t end = _size;
//end和pos比较时会进行整型提升!
while (end >= pos && end != npos)
{
_str[end + len] = _str[end];
end--;
}
//插入数据
for (int i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
📌resize()函数
resize()函数的作用是调整字符串的大小_size为n,其函数定义如下:编辑 该函数在执行的时候会面临三种情况,如下图所示:
编辑
对于情况1,我们直接在n位置填入'\0'即可,对于情况2和3,我们选择先扩容,再在后面填入数据,综上,代码如下:
void resize(size_t n, char ch = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
reserve(n);
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
📌erase()函数
erase()函数的作用是擦除字符串中pos位置的n个字符.
我们可能会遇到两种情况:一种是从pos擦除n个字符后后面还有有效字符,这种情况下我们要考虑挪动数据,另一种是直接从pos位置删除掉后面的所有字符,这种情况下我们可以考虑直接在pos位置放上'\0'即可.
综上,erase()函数实现代码如下:
void erase(size_t pos, size_t len = npos)
{
assert(pos <= _size);
//判断len是不是为缺省参数 或者 pos+len已经超出了_size的范围,如果是,那么就代表要删完
if (len == npos || pos + len >= _size)
{
//要删完,直接在这个位置放一个\0就行
_str[pos] = '\0';
_size = pos;
_str[_size] = '\0';
}
else
{
size_t end = pos + len;
while (end <= _size)
{
_str[pos++] = _str[end++];
}
_size -= len;
}
}
📌find()函数
find()函数定义如下:
编辑
find()函数实现两个,一个用于在pos位置之后查找一个字符,一个用于在pos位置查找一个字串(利用库函数strstr()实现,有兴趣的朋友可以研究一下BM算法和KMP算法).
综上,代码如下:
//查找某一字符的位置
//check some character's position
size_t find(char ch, size_t pos = 0)
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
//没找着,返回npos
//if character is not in this string,return npos
return npos;
}
//查找子串
//check some substring's position
size_t find(const char* str, size_t pos = 0)
{
assert(pos < _size);
//相关算法:BM算法和KMP算法
const char* ptr = strstr(_str + pos, str);
if (ptr)
{
//if substring is in this string,return this substring's posision
return ptr - _str;
}
else
{
//没找着,返回npos
//if substring is not in this string,return npos
return npos;
}
}
📌substr()函数
substr()函数定义如下:
编辑
substr()函数的实现算法逻辑是:
• 先确定好截取字串的个数,因为n不一定完全有效
• 创建string变量用于存储字串
• 设置变量容量为n
• 使用循环将主串需要的字符逐一+=到子串上去
• 返回存储了子串的变量
代码如下:
//获取一个子串
string substr(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
size_t n = len;
if (len == npos || pos + len > _size)
//如果len的长度是npos(非常大)或者pos+len的长度已经超出了_size的大小
//这两种情况都意味着要获取的字串是从pos开始直到字符串结尾
{
n = _size - pos;
}
string tmp;
tmp.reserve(n);
//i不是从0开始的,但是n长度是绝对长度的,所以判断条件要注意
for (size_t i = pos; i < pos + n; i++)
{
tmp += _str[i];
}
return tmp;
}
📌clear()函数
clear()函数的功能是清空当前类对象的内容,它实现起来非常简单,就是给类对象的首字符插入一个'\0',然后将类对象的_size置为0即可.
代码如下:
void clear()
{
_str[0] = '\0';
_size = 0;
}
📌swap()函数
swap()函数需要完成的是将两个string类对象的内容做交换,而string类对象又包含三个内容:1._str 2._size 3._capacity ,所以我们分别交换这三个内容即可完成两个string类的交换
,我们可以借助库函数swap()函数来完成这一功能:
编辑
综上,swap()代码如下:
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
🎏实现string类运算符重载
📌operator []运算符重载
operator []运算符的作用是让string类对象变得可以像数组一样访问,它接收一个size_t类型的值作为参数pos,并返回string类_str字符串中pos位置的字符.
无const修饰的类对象
对于非const修饰的类对象调用operator []运算符,我们返回的是可以读也可以修改的pos位置的字符的引用.代码如下:
char& operator[](size_t pos)//非const修饰类对象,可以读写
{
assert(pos < _size);//在一开始判断pos位置是否在_size范围内,如果不是,则说明访问越界
return _str[pos];
}
有const修饰的类对象
对于const修饰的类对象调用operator []运算符,我们返回的是可以读但不能修改的pos位置的字符的引用.代码如下:
const char& operator[](size_t pos) const //const修饰类对象,只能读
{
assert(pos < _size);//在一开始判断pos位置是否在_size范围内,如果不是,则说明访问越界
return _str[pos];
}
📌operator +=运算符重载
operator +=运算符的作用是在当前值的末尾附加其他字符来拓展字符或字符串.我们分别实现两个operator +=重载函数,一个用于追加字符,一个用来追加字符串.需要注意的是,+=运算符的返回值是+=后的结果类对象,所以+=运算符重载函数的返回值是字符串引用类型,即string&.
由于我们之前已经实现过push_back()函数和append()函数了,所以这里只需要复用一下这两个函数即可,综上,代码如下:
//+=一个字符
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
//+=一个字符串
string& operator+=(const char* str)
{
append(str);
return *this;
}
📌operator<<运算符重载
我们曾经在Data类中详细分析过对自定义类如何重载流插入和流提取函数,如下:编辑
对于string类的流插入函数,我们只需要将string类对象中的字符逐一插入到ostream(流插入)对象中即可,然后因为流插入运算符是有返回值的,因此我们将流插入对象作为返回值返回.
综上,代码如下:
//注意,ostream必须使用&返回,因为采用传值返回的话ostream会拷贝
//而ostream类型是禁止拷贝的
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
注意,使用c_str()函数和使用流插入函数打印主要区别如下:
• C的字符数组,以'\0'为终止算长度
• string不看'\0',以_size为终止算长度
编辑
📌operator>>运算符重载
流提取函数的作用是从终端输入设备提取字符到类对象中,但在一开始我们就面临一个问题,就是我们对输入的数据是未知的,即不知道它的内容,也不知道它有多长,所以稳妥的方法就是我们一个字符一个字符从流中提取内容,然后按照我们正常的扩容逻辑,可能如果插入128个字符就要扩容7次,而扩容又每次都要拷贝转移数据到新开辟的空间,这样会导致非常多的浪费,所以我们先开一个128的字符数组,然后将读取到的字符数据先累积到字符数组里,当字符数组满了之后,再统一一次性开容量,然后加入到类对象的空间中去,这样比较节省资源.
流提取这里相比于流插入逻辑复杂一些,有很多细节需要我们注意,见代码注释:
istream& operator>>(istream& in, string& s)
{
//因为我们要实现每次流提取的内容都对之前的内容是覆盖过的,并且这里的string类对象参数s是引用
//它不会随着函数退出而销毁,所以这里需要手动调用一下clear()函数
s.clear();
//正常in对象是读不到空格/换行的,因为它在设计的时候自动的将空格符和换行符当成了字符输入的分割符
//所以在in对象读的时候就会忽略空格/换行,导致我们的空格/换行符判定无效,要解决就使用get()函数
//get()函数就是无论是什么内容它都认为是有效字符然后读取出来
char ch = in.get();
//处理掉缓冲区前面的空格或者换行:一开始读到换行或者空格不处理继续往后读就行
while (ch == ' ' || ch == '\n')
{
ch = in.get();
}
//in>>ch;
//上面这行代码不适用,因为in对象认为接收到空格或者换行本个字符串的输入就截止了,所以要用get()函数
char buff[128];
int i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127)
{
buff[i] = '\0';
s += buff;
//in>>ch;
//ch = in.get();
i = 0;
}
ch = in.get();
}
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
📌operator <运算符重载
注意:后面的6个比较运算符重载函数都属于类的只读函数,对于只读函数我们应该主动在函数后面加上const修饰this指针,以便const修饰的对象也可以正常调用该类型的函数!
string类的比较大小和C语言字符串一样,是通过ascii码来比较的.但是我们不能使用C语言库中的strcmp()直接来比较string类的大小,因为strcmp()的比较逻辑是按照'\0'为终止字符的,
但string类并不是以'\0'为终止,而是以_size的大小为终止的.两种比较逻辑如下图所示:
编辑
综上所述,代码如下:
bool operator<(const string& s)const
{
//return strcmp(_str, s._str) < 0;
//会有中间含'\0'的字符串比较的问题,所以用memcmp
//memcmp比较的长度应该是短的字符串长度+1
// 不能用size+1,因为'\0'不一定算小字符,有些汉字的u16/u18编码可能会是负数
//return memcmp(_str,s._str,_size < s._size ? _size : s._size);
//先比较短的字符串长度范围内的值
size_t i1 = 0;
size_t i2 = 0;
while (i1 < _size && i2 < s._size)
{
if (_str[i1] < s._str[i2])
{
return true;
}
else if (_str[i1] > s._str[i2])
{
return false;
}
else
{
i1++;
i2++;
}
}
//当走到这个位置时,说明至少其中一个结束了,并且另一个在前面部分一直和它是相等的
//那么如果此时i1走到尽头,但i2没走到尽头,就说明i1是小于i2的,因此返回ture
//否则i1>=i2,返回false
//注意,在这里也不能比较size+1,因为'\0'不一定算小字符
//虽然在ascii编码中它是0,但有些汉字的u16/u18编码可能会是负数
return (i1 == _size && i2 != s._size);
}
还有一种复用库函数memcpy()函数版本的实现方式:
bool operator<(const string& s)const
{
//先比较短的字符串长度范围内的值
bool ret = memcmp(_str,s._str,_size < s._size ? _size : s._size);
//ret==0说明前面部分两个字符串都相等,这时候比长度就行
//否则说明两个字符串前面都不相等,返回前面的比较结果ret是否<0就行
return ret == 0 ? _size < s._size : ret < 0;
}
📌operator ==运算符重载
operator ==运算符重载的作用是判断两个string类对象是否相等,我们可以先判断两个string类的长度是否相等,再复用memcpy()函数判断其中的字符串是否相等.
代码如下:
bool operator==(const string& s)const
{
return _size==s._size && memcmp(_str, s._str, _size) == 0;
}
📌operator <=运算符重载
因为我们前面已经实现<和==运算符了,下面我们只需要复用前面实现过的逻辑就可以完成<=运算符重载了,代码如下:
bool operator<=(const string& s)const
{
return (*this < s || *this == s);
}
📌operator >运算符重载
因为我们前面已经实现<=运算符了,下面我们只需要复用前面实现过的逻辑就可以完成>运算符重载了,代码如下:
bool operator>(const string& s)const
{
return !(*this <= s);
}
📌operator >=运算符重载
因为我们前面已经实现<运算符了,下面我们只需要复用前面实现过的逻辑就可以完成>=运算符重载了,代码如下:
bool operator>=(const string& s)const
{
return !(*this < s);
}
📌operator !=运算符重载
因为我们前面已经实现==运算符了,下面我们只需要复用前面实现过的逻辑就可以完成!=运算符重载了,代码如下:
bool operator!=(const string& s)const
{
return !(*this == s);
}
🎏实现string类迭代器
C++中,我们也可以使用迭代器来访问string对象的字符,在string中,迭代器的底层是使用指针来实现的,如下,我们使用typedef重命名char*类型为iterator:
typedef char* iterator;
当然,我们也需要考虑到为const修饰类对象实现迭代器,如下,我们使用typedef重命名const char*类型为const_iterator:
typedef const char* const_iterator;
定义好迭代器类型后,接下来,就可以实现迭代器相关的函数了:
📌begin()函数
begin()函数的作用是返回指向_str字符串第一个字符的迭代器,如下图所示,即_str的首地址:
编辑
代码如下:
iterator begin()
{
return _str;
}
对于const修饰对象而言,begin()函数返回的迭代器也要是const类型的,同时,形参this指针也要加上const修饰才能够和const修饰的类对象参数匹配,综上,代码如下:
const_iterator begin()const
{
return _str;
}
📌end()函数
end()函数的作用是返回指向_str字符串最后一个有效字符(即不包括'\0')后一个理论字符位置的迭代器,如下图所示,即_str+_size位置的地址:
编辑
代码如下:
iterator end()
{
return _str + _size ;
}
对于const修饰对象而言,end()函数返回的迭代器也要是const类型的,同时,形参this指针也要加上const修饰才能够和const修饰的类对象参数匹配,综上,代码如下:
const_iterator end()const
{
return _str + _size;
}
📌迭代器测试
我们创建一个string变量st1,然后创建一个迭代器变量it,给它赋值为st1.begin(),接着设置while循环,判断it是否!=st1.end(),如果不相等,则it继续向后遍历,直到2者相等,代码如下:
void test1()
{
mfc::string st1("hello world");
mfc::string::iterator it = st1.begin();
while (it != st1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
int main()
{
test1();
return 0;
}
代码测试结果如下,我们成功使用迭代器遍历了string类对象:
编辑
接下来我们再测试以下使用迭代器修改string类对象的内容:
void test2()
{
mfc::string st1("hello world");
mfc::string::iterator it = st1.begin();
while (it != st1.end())
{
(*it)++;
cout << *it << " ";
it++;
}
cout << endl;
}
int main()
{
test2();
return 0;
}
代码测试结果如下,我们成功使用迭代器遍历并修改了string类对象:
编辑
最后我们测试一下const修饰的string类对象的迭代器遍历,代码如下:
void test3()
{
mfc::string const st1("hello world");
mfc::string::const_iterator it = st1.begin();
while (it != st1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
int main()
{
test3();
return 0;
}
测试结果如下,我们成功使用const迭代器遍历了const修饰的类对象:
编辑
📌迭代器的衍生品——范围for
c++中范围for的定义如下:编辑
因为范围for的底层实现原理是依靠迭代器来实现的,所以当我们实现的类支持迭代器时,就自动支持了范围for,我们可以直接使用范围for来遍历类对象成员,如:
void test4()
{
mfc::string st1("hello world");
for(auto ch : st1)
{
cout << ch << " ";
}
cout << endl;
}
int main()
{
test4();
return 0;
}
范围for测试结果如下:
编辑
三.项目完整代码
我们将程序运行的代码分别在两个工程文件中编辑,完整代码如下:
test.cpp文件
注:该文件主要是用来测试我们已完成的代码是否能够合理的完成我们的想要的功能,主要是看个人需求,因此不同的人的测试代码可能不相同,以下代码仅供参考.
#include"string.h"
void test1()
{
mfc::string st1("hello world");
mfc::string::iterator it = st1.begin();
while (it != st1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
void test2()
{
mfc::string st1("hello world");
mfc::string::iterator it = st1.begin();
while (it != st1.end())
{
(*it)++;
cout << *it << " ";
it++;
}
cout << endl;
}
void test3()
{
mfc::string const st1("hello world");
mfc::string::const_iterator it = st1.begin();
while (it != st1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
void test4()
{
mfc::string st1("hello world");
for(auto ch : st1)
{
cout << ch << " ";
}
cout << endl;
}
void test5()
{
int a = -1;
size_t b = 0;
if (a > b)
{
cout << "a>b:"<<a<<">"<<b << endl;
}
else
{
cout << "a<b:" << a << "<" << b << endl;
}
}
void test6()
{
mfc::string st1("hello world");
st1 += '\0';
st1 += "!!!";
cout << st1.c_str() << endl;
cout << st1 << endl;
}
void test7()
{
mfc::string st1("hello");
mfc::string st2("yyyyyyyyxxxxxxx");
cout << st1 << endl;
cout << st2 << endl << endl;
st1 = st2;
cout << st1 << endl;
cout << st2 << endl;
}
int main()
{
test7();
return 0;
}
string.h文件
注:该文件中包含了string类的完整模拟实现代码,如需使用,请留意命名空间的限制.
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
#include<string>
#include<assert.h>
using namespace std;
namespace mfc
{
class string
{
public:
//迭代器
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size ;
}
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
//构造
string(const char* str = "")
:_size(strlen(str))
,_capacity(_size)
{
//只是给_str开了空间
_str = new char[_capacity + 1];//多开一个空间放'\0'
//这步才是给_str放入数据
memcpy(_str, str,_size+1);
}
//拷贝构造
string(const string& s)
{
_str = new char[s._capacity + 1];
memcpy(_str, s._str, s._size + 1);
_size = s._size;
_capacity = s._capacity;
}
//拷贝构造(这个遇到中间有'\0'的时候会有bug,后面的不会拷贝)
/*string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
swap(tmp);
}*/
////传统写法
//string& operator=(const string& s)
//{
// if (this != &s)
// {
// char* tmp = new char[s._capacity + 1];
// memcpy(tmp, s._str, s._size + 1);
// delete[] _str;
// _str = tmp;
// _size = s._size;
// _capacity = s._capacity;
// }
// return *this;
//}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//现代
//string& operator=(const string& s)
//{
// if (this != &s)
// {
// string tmp(s);
// //this->swap(tmp);
// swap(tmp);
// }
// return *this;
//}
//未来
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
~string()
{
//不是空再释放
if (_str!=nullptr)
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
}
const char* c_str() const//使const对象也可以调用
{
return _str;
}
size_t size() const//使const对象也可以调用
{
return _size;
}
char& operator[](size_t pos)//读写
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const//只读
{
assert(pos < _size);
return _str[pos];
}
//增删查改
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
memcpy(tmp, _str, _size + 1);
//这里使用strcpy拷贝的话就可能出现对于有\0的字符串的拷贝错误现象
delete[] _str;//一定要记得释放_str!!!!!!!
_str = tmp;
_capacity = n ;
}
}
void resize(size_t n, char ch = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
reserve(n);
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
void push_back(char ch)
{
//查满扩容
if (_size == _capacity)
{
//2倍扩容
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
//尾插一个字符
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
//扩容,且至少到_size+len,不能是二倍扩容!
reserve(_size + len );//不+1,在reserve内部考虑
}
memcpy(_str + _size, str, len + 1);
_size += len;
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
//pos位置插入n个char
void insert(size_t pos, size_t n, char ch)
{
//判断pos是否合理
assert(pos <= _size);
//判断容量
if (_size + n > _capacity)
{
reserve(_size + n);
}
//挪动后面的数据
size_t end = _size;
//end和pos比较时会进行整型提升!
while (end >= pos && end != npos)
{
_str[end + n] = _str[end];
end--;
}
//插入数据
for (size_t i = 0; i < n; i++)
{
_str[pos + i] = ch;
}
_size += n;
}
//pos位置插入一个字符串
void insert(size_t pos,const char* str)
{
//判断pos是否合理
assert(pos <= _size);
//判断容量
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
//挪动后面的数据
size_t end = _size;
//end和pos比较时会进行整型提升!
while (end >= pos && end != npos)
{
_str[end + len] = _str[end];
end--;
}
//插入数据
for (int i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
void erase(size_t pos, size_t len = npos)
{
assert(pos <= _size);
//判断len是不是为缺省参数 或者 pos+len已经超出了_size的范围,如果是,那么就代表要删完
if (len == npos || pos + len >= _size)
{
//要删完,直接在这个位置放一个\0就行
_str[pos] = '\0';
_size = pos;
_str[_size] = '\0';
}
else
{
size_t end = pos + len;
while (end <= _size)
{
_str[pos++] = _str[end++];
}
_size -= len;
}
}
//查找某一字符的位置
//check some character's position
size_t find(char ch, size_t pos = 0)
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
//没找着,返回npos
//if character is not in this string,return npos
return npos;
}
//查找子串
//check some substring's position
size_t find(const char* str, size_t pos = 0)
{
assert(pos < _size);
//相关算法:BM算法和KMP算法
const char* ptr = strstr(_str + pos, str);
if (ptr)
{
//if substring is in this string,return this substring's posision
return ptr - _str;
}
else
{
//没找着,返回npos
//if substring is not in this string,return npos
return npos;
}
}
//获取一个子串
string substr(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
size_t n = len;
if (len == npos || pos + len > _size)
//如果len的长度是npos(非常大)或者pos+len的长度已经超出了_size的大小
//这两种情况都意味着要获取的字串是从pos开始直到字符串结尾
{
n = _size - pos;
}
string tmp;
tmp.reserve(n);
//i不是从0开始的,但是n长度是绝对长度的,所以判断条件要注意
for (size_t i = pos; i < pos + n; i++)
{
tmp += _str[i];
}
return tmp;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
bool operator<(const string& s)const
{
//return strcmp(_str, s._str) < 0;
//会有中间含'\0'的字符串比较的问题,所以用memcmp
//memcmp比较的长度应该是短的字符串长度+1
// 不能用size+1,因为'\0'不一定算小字符,有些汉字的u16/u18编码可能会是负数
//return memcmp(_str,s._str,_size < s._size ? _size : s._size);
//先比较短的字符串长度范围内的值
size_t i1 = 0;
size_t i2 = 0;
while (i1 < _size && i2 < s._size)
{
if (_str[i1] < s._str[i2])
{
return true;
}
else if (_str[i1] > s._str[i2])
{
return false;
}
else
{
i1++;
i2++;
}
}
//当走到这个位置时,说明至少其中一个结束了,并且另一个在前面部分一直和它是相等的
//那么如果此时i1走到尽头,但i2没走到尽头,就说明i1是小于i2的,因此返回ture
//否则i1>=i2,返回false
//注意,在这里也不能比较size+1,因为'\0'不一定算小字符
//虽然在ascii编码中它是0,但有些汉字的u16/u18编码可能会是负数
return (i1 == _size && i2 != s._size);
}
//bool operator<(const string& s)const
//{
// //先比较短的字符串长度范围内的值
// bool ret = memcmp(_str,s._str,_size < s._size ? _size : s._size);
// //ret==0说明前面部分两个字符串都相等,这时候比长度就行
// //否则说明两个字符串前面都不相等,返回前面的比较结果ret是否<0就行
// return ret == 0 ? _size < s._size : ret < 0;
//}
//后面复用小于得到其他六个
bool operator==(const string& s)const
{
return _size==s._size && memcmp(_str, s._str, _size) == 0;
}
bool operator<=(const string& s)const
{
return (*this < s || *this == s);
}
bool operator>(const string& s)const
{
return !(*this <= s);
}
bool operator>=(const string& s)const
{
return !(*this < s);
}
bool operator!=(const string& s)const
{
return !(*this == s);
}
private:
char* _str;
size_t _size;
size_t _capacity;
static size_t npos;
};
size_t string::npos = -1;
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
//处理掉缓冲区前面的空格或者换行:一开始读到换行或者空格不处理继续往后读就行
while (ch == ' ' || ch == '\n')
{
ch = in.get();
}
//in>>ch;
//因为in对象认为接收到空格或者换行本个字符串的输入就截止了
char buff[128];
int i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127)
{
buff[i] = '\0';
s += buff;
//in>>ch;
//ch = in.get();
i = 0;
}
ch = in.get();
}
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
};
结语
希望这篇string类的模拟实现详解能对大家有所帮助,欢迎大佬们留言或私信与我交流.
学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!
相关文章推荐
【C++】 类 的六大默 认 成 员 函数及其特性 (万字 详 解 )
编辑
- 点赞
- 收藏
- 关注作者
评论(0)