string模拟实现
- 引言(实现概述)
- string类方法实现
- 默认成员函数
- 构造函数
- 拷贝构造
- 赋值运算符重载
- 析构函数
- 迭代器
- begin
- end
- 容量
- size、capacity、empty
- reserve
- resize
- 访问元素
- operator[]
- 修改
- insert
- 插入字符
- 插入字符串
- append
- push_back
- operator+=
- erase
- clear
- swap
- 比较运算符重载
- operator<
- operator==
- 其他
- 查找
- 查找字符
- 查找字符串
- 非成员函数
- operator<<
- operator>>
- 源码概览
- 总结
引言(实现概述)
在上一篇文章中,我们介绍了string类的使用:
戳我康string类的使用详解哦
在本篇文章中就要来模拟实现一下string类,以帮助我们更好的理解与使用string
在我们模拟实现的string中,要具备库中string所具有的主要接口,例如:默认成员函数、迭代器、容量、元素访问、运算符重载、非成员函数。其中只实现这些函数的常用重载形式。
我们将模拟实现的string类放在我们创建的命名空间内,以防止与库中的string发生命名冲突。在以后的STL模拟实现时,也会将其放在命名空间内。
string类的实现与之前C语言部分的顺序表类似,结合类和对象的知识,这个string类的属性有:指向堆中一块用于存放字符序列的空间的指针(_str
)、字符序列的字符个数(_size
)、字符序列的容量(_capacity
)。
string类方法实现
默认成员函数
构造函数
在构造函数部分,就只实现用常量字符串构造string对象
在这个构造函数中,参数类型就是const char*
,给这个参数一个空串""
作为缺省值;
在函数内部首先assert
判断参数是否为空指针;
然后令_size
的值等于常量字符串str
的长度,令_capacity
等于_size
的值;
然后new
一块空间,将这块空间的指针赋给_str
,这里需要注意的是,strlen
计算字符串长度时,是以'\0'
为结束标志的,所以在动态开辟空间时,需要开辟_capacity + 1
个char的空间;
最后使用memcpy
将常量字符串中的数据拷贝到刚刚申请的空间中(这里拷贝时也需要将'\0'
拷贝进去,所以要拷贝_size + 1
个字节):
string(const char* str = "")
{
assert(str);
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
memcpy(_str, str, _size + 1);
}
拷贝构造
拷贝构造是构造函数的重载,参数类型为const string&
,如果不是引用就会导致无穷递归调用
实现拷贝构造时,不能直接将原对象的_str
直接赋值给新对象的_str
,否则就会导致浅拷贝,这样在析构时,同一块空间就要被析构两遍,显然就会导致崩溃。所以我们需要新申请一块空间后将原对象的字符序列拷贝到新的空间。
先将原对象的_size 与 _capacity 直接赋值给新对象
然后new
一块空间,将这块空间的指针赋给_str
,这里需要注意的是,在我们实现的string类中,_capacity
是不包括'\0'
的,所以在动态开辟空间时,需要开辟_capacity + 1
个char的空间;
最后,使用memcpy将原对象中的字符序列拷贝到刚刚新开辟的空间中。
这里需要注意的是,C字符串的结束标志为'\0'
,但string对象没有结束标志,它的数据个数就是_size
的值,所以当这个字符序列的中间出现'\0'
时,使用strcpy
就不会拷贝'\0'
后面的数据,所以这里使用包括后面都使用memcpy
。
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
memcpy(_str, s._str, s._size + 1);
}
赋值运算符重载
在实现赋值运算符重载时,也存在深浅拷贝的问题,我们当然可以像上面的拷贝构造那样申请一块空间然后完成拷贝,但是那样的写法有点麻烦,于是就有了现代版本:
现代版本的参数类型为string
,而不是引用,这就使得string对象在传参时会生成一个临时对象,我们将这个临时对象与要替换的对象*this
互换,就实现了将一个对象赋值到了*this
,最后返回*this
即可,临时对象会在函数栈帧销毁时析构。
(这里的交换需要用到swap
函数,这个函数后面会实现)
//string& operator=(const string& s); //老版本
string& operator=(string s)
{
swap(s);
return *this;
}
析构函数
析构函数只需使用delete[]
释放_str
指向的堆中的空间即可,还可以顺带将_size
与_capacity
置0:
~string()
{
_size = 0;
_capacity = 0;
delete[] _str;
}
迭代器
前面提到过,string的迭代器就是原生指针,所以string中的 iterator
就是char*
,const_iterator
就是const char*
,我们只需要使用typedef
将char*
与const char*
重命名即可:
typedef char* iterator;
typedef const char* const_iterator;
需要注意的是,因为在string类外也需要使用迭代器,所以这样的重命名应在pubilc
中。
begin
begin
获取的是字符序列首元素的地址,有两个重载版本,即对于非const对象返回iterator,对于const对象返回const_iterastor,首元素的地址就是_str
。
需要注意的是:const版本需要使用const
修饰this
指针
string::iterator begin()
{
return _str;
}
string::const_iterator begin() const
{
return _str;
}
end
end
获取的是字符序列最后一个元素下一个位置的地址,有两个重载版本,即对于非const对象返回iterator,对于const对象返回const_iterastor,最后一个元素下一个位置的地址就是_str + _size
。
string::iterator end()
{
return _str + _size;
}
string::const_iterator end() const
{
return _str + _size;
}
容量
size、capacity、empty
这三个成员函数的实现逻辑类似:
size
用于获取string对象中字符序列的元素个数,返回_size
即可;
capacity
用于获取string对象的容量,返回_capacity
即可;
empty
用于判断string对象是否为空,若为空返回true,否则返回false:
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
bool empty() const
{
if (_size == 0)
{
return true;
}
return false;
}
reserve
reserve
用于修改string对象的容量,这个函数只有一个参数n,表示要扩容到多少个char。
reserve在扩容时,当n小于当前对象的容量时,reserve会将容量调整为比n
大的值,所以在模拟实现时,当n
小于容量时,将不做任何事。所以先判断n
是否大于_capacity
;
C++中使用new不能像realloc一样实现扩容,必须使用new新开一块空间(开空间时,由于_capacity
没有包括'\0'
,所以要开n + 1
个char的空间);
再将原空间中的数据拷贝到新空间,然后释放原空间;
然后使_str
指向新空间;
最后将_capacity
的值改为n
:
void reserve(size_t n)
{
if (n > _capacity)
{
char* newstr = new char[n + 1]{ 0 };
memcpy(newstr, _str, _size + 1);
delete[] _str;
_str = newstr;
_capacity = n;
}
}
resize
resize
用于修改string对象中字符序列的个数,当参数n
小于size就删,大于size则用指定的字符c
补足。
首先判断,当n
大于_size
的值时,就需要扩容,复用reserve扩容至n;
然后循环,将下标为_size
到n - 1
位置的元素改为指定的c
;
最后在末尾加上'\0'
,并更新_size
的值
当n
小于_size
的值时,直接在下标为n的位置加上'\0'
,并更新_size
即可:
void resize(size_t n, char c)
{
if (n > _size)
{
if (n > _capacity)
{
reserve(n + _size);
}
for (size_t i = _size; i < n; ++i)
{
_str[i] = c; //这里的[]是访问数组元素,并非运算符重载的调用,所以不会越界
}
_size = n;
_str[_size] = '\0';
}
else
{
_size = n;
_str[_size] = '\0';
}
}
访问元素
operator[]
通过重载[]
可以实现像数组下标一样访问string对象中字符序列的元素,有两个重载版本,即对普通对象与const对象。函数有一个参数index
即要访问元素的下标。
首先assert
判断参数index
是否越界;
然后返回_str[index]
即可:
char& operator[](size_t index)
{
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index) const
{
assert(index < _size);
return _str[index];
}
修改
insert
insert
实现将一个字符或字符串插入到string对象的pos
位置,实现两个重载版本,即在pos
位置插入一个字符与一个字符串:
插入字符
首先assert
判断pos是否越界,pos
为无符号整数,所以只需要判断是否大于_size
即可;
然后当_size
等于_capacity
时,即空间已满,需要扩容;
扩容时,当_capacity
的值为0时扩容到4,不为0时二倍扩容;
然后就需要循环,将pos
位置后的数据全部向后移动(需要注意的是循环的终止条件,当pos
为0时,若end
的初始值为_size
且为end
给end + 1
赋值,循环的终止条件就为end >= pos
。而pos为size_t,当end与pos比较时,会转化为size_t而永远不可能小于0,故end的初始值为_size + 1
,将end + 1
给前赋值,终止条件就为end > pos
);
最后将c
填充到pos
位置,并更新_size
:
string& insert(size_t pos, char c)
{
assert(pos <= _size);
if (_size == _capacity)
{
if (_capacity != 0)
{
reserve(2 * _capacity);
}
else
{
reserve(4);
}
}
size_t end = _size + 1;
while (end > pos) //pos为size_t,当end与pos比较时,会转化为size_t而永远不可能小于0.故将end+1,后给前赋值
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = c;
++_size;
return *this;
}
插入字符串
插入字符串的逻辑与插入字符类似:
首先判断pos
是否越界,并判断是否需要扩容,当容量小于_size + len
时就需要扩容(len
为插入字符串的长度,这里可以直接调用reserve,因为reserve中会判断参数是否大于原容量);
然后将pos位置后的数据全部向后移动len个位置(依旧需要注意终止条件:必须为 end > pos + len - 1
,若为end >= pos+len
时,当在0位置插入一个空串就会导致死循环,因为无符号整型不可能小于0。当为end > pos + len - 1
时,遇到上面的情况,0 - 1为 -1
,对无符号整型就是一个很大的数,将直接不进入循环)
然后循环将字符串中的数据拷贝到pos
位置,并更新_size
:
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
reserve(len + _size);
size_t end = _size + len;
while (end > pos + len - 1) //写成end >= pos+len就会有问题(在0位置插一个"")
{
_str[end] = _str[end - len];
--end;
}
for (size_t i = 0; i < len; ++i)
{
_str[pos + i] = str[i];
}
_size += len;
return *this;
}
append
append
实现在string对象后追加一个字符串:
首先判断str是否为空指针,并判断是否需要扩容(当newlenth > _capacity
时即需要扩容,当然也可以交给reserve
中判断);
然后使用strcpy将str中的数据拷贝到_str + _size的后面(这里不需要使用memcpy
,因为这里的字符串拷贝就是按照'\0'
为结束标志的);
最后更新_size
:
//尾追加
void append(const char* str)
{
assert(str);
size_t newlenth = _size + strlen(str);
if (newlenth > _capacity)
{
reserve(newlenth);
}
strcpy(_str + _size, str);
_size = newlenth;
}
push_back
push_back
用于在string对象末尾添加一个字符
实现时首先判断是否需要扩容(与insert
插入字符时的逻辑一致);
然后将c放在_str
的_size
位置,并更新_size
;
最后需要手动补上'\0'
:
//尾插
void push_back(char c)
{
if (_size == _capacity)
{
if (_capacity != 0)
{
reserve(2 * _capacity);
}
else
{
reserve(4);
}
}
_str[_size] = c;
++_size;
_str[_size] = '\0';
}
operator+=
operator+=
即在string对象的末尾追加数据,实现两个重载版本,即追加字符与追加字符串:
实现时,复用append
与push_back
即可(当然,上面的append与push_back也可以借助insert
实现)
string& operator+=(char c)
{
push_back(c);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
erase
erase
实现删除pos
位置上的len
个元素
当len
等于npos
时,即将pos
位置后全删,将pos
位置改为'\0'
并更新_size
即可(npos
为无符号整型的-1
,即一个很大的数);
否则,就需要循环,将pos + len
位置后的数据全部向前移动 len
个位置,覆盖原数据实现删除,最后更新_size
:
// 删除pos位置上的len个元素,并返回
string& erase(size_t pos, size_t len)
{
if (len == npos)
{
_size = pos;
_str[_size] = '\0';
}
for (size_t i = 0; i < _size - pos; ++i)
{
_str[pos + i] = _str[pos + len + i];
}
_size -= len;
return *this;
}
clear
clear
即清空string对象中的数据,只需要将0位置改为'\0'
,并将_size
更新为0即可:
void clear()
{
_size = 0;
_str[_size] = '\0';
}
swap
swap
实现交换两个string对象
在之前的swap函数,包括算法库中的swap函数均是通过临时变量的方式交换的。但是对于string对象而言,要创建临时对象通过三次赋值来交换的话,就会产生三次深拷贝,十分影响效率。
在实现交换时,其实没有必要出现深拷贝,string对象中有_str指向一块空间中存储数据,只要交换string对象中的这个存储数据的指针即可实现交换数据,所以将string的对象的属性分别实现交换即可,交换_size
、_capacity
、_str
即可:
void swap(string& s)
{
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
std::swap(_str, s._str);
}
比较运算符重载
在实现比较运算符重载时,我们其实只需要实现两种,即<
与==
,其他的运算符重载通过复用这两种即可,此类函数都需要使用const修饰this以适配const对象:
operator<
operator<
实现两个string对象的比较:当第一个对象小于第二个对象时,返回true
,否则返回false
我们可以for
循环逐字节判断,循环的终止条件为两个string对象_size
的较小值;
当遇到对应字符不相等的情况时,直接返回true
或false
;
当循环结束,说明前面的元素都是相等的。此时,哪个对象的_size
较大,则该对象较大:
bool operator<(const string& s) const
{
for (size_t i = 0; i < (_size < s._size ? _size : s._size); ++i)
{
if (_str[i] < s._str[i])
return true;
if (_str[i] > s._str[i])
return false;
}
if (_size < s._size)
return true;
else
return false;
}
operator==
operator==
用于判断两个string对象是否相等,相等返回true
,否则返回false
当两个string对象的_size
不同时,直接返回false
;
然后循环遍历两个对象,遇到对应位置不相同的,直接返回false
;
最后,出循环说明均相等,返回true
:
bool operator==(const string& s) const
{
if (_size != s._size)
return false;
for (size_t i = 0; i < _size; ++i)
{
if (_str[i] != s._str[i])
return false;
}
return true;
}
其他
其他函数,根据比较的逻辑复用即可:
bool operator<=(const string& s) const
{
if (*this < s || *this == s)
return true;
else
return false;
}
bool operator>(const string& s) const
{
if (!(*this <= s))
return true;
else
return false;
}
bool operator>=(const string& s) const
{
if (!(*this < s))
return true;
else
return false;
}
bool operator!=(const string& s) const
{
if (!(*this == s))
return true;
else
return false;
}
查找
find
用于在string对象中查找是否存在某字符或某字符串,若存在就返回其第一次出现的位置,否则返回npos
,有两个重载版本,即查找字符与查找字符串:
查找字符
查找字符时,即使用循环从pos
位置开始遍历string对象中的数据,当遇到与c
相等的字符时,就返回该位置。若出循环,就表示没有找到,返回npos
:
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
查找字符串
在string对象中查找字符串时,可以使用之前C语言时学过的strstr
函数,用于查找字串。(当第2个参数为第1个参数的子串时,返回在其中的第一个位置的地址,否则返回空指针)
首先assert
判断s
是否为空指针,以及pos
是否越界;
然后调用strstr,第一个参数为_str
,第二个参数为s
,并创建一个指针pchar
来接收返回值;
当pchar
为空时,返回npos
,当pchar - _str
的值不小于pos
时,返回该差,否则返回npos
:
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos) const
{
assert(s);
assert(pos < _size);
char* pchar = strstr(_str, s);
if (pchar == nullptr)
{
return npos;
}
if ((size_t)(pchar - _str) >= pos)
{
return pchar - _str;
}
return npos;
}
非成员函数
非成员函数中只实现流插入与流提取运算符的重载(operator<<
与operator>>
):
在之前的日期类实现中,我们使用友元函数,实现在这两个函数中可以访问对象的属性。但在string类中,由于之前实现过访问元素的operator[]
,所以可以不使用友元就可以实现在这两个函数中访问string对象的元素:
operator<<
在operator<<
中, 我们只需要将string对象s
中的元素依次流入到ostream
的对象_cou
t即可:
ostream& operator<<(ostream& _cout, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
_cout << s[i];
}
return _cout;
}
operator>>
在向内存中输入数据时,我们当然可以逐字符的+=
,但是这样会造成多次的扩容而影响效率。
我们可以直接创建一个128字节的数组来转存数据,当在这个数组中存满后再将这个数组中的数据+=
到string对象中,然后清空数据继续接收数据,等到全部接收完毕后,将其中剩余的元素再**+=**到string对象后即可:
istream& operator>>(istream& _cin, string& s)
{
s.clear();
char ch = _cin.get();
//清除缓冲区中的空格与换行
while (ch == ' ' || ch == '\n')
{
ch = _cin.get();
}
//定义一个128的字符数组
char temp[128] = { 0 };
int i = 0;
while (ch != ' ' && ch != '\n')
{
if (i == 127)
{
s += temp;
i = 0;
}
temp[i] = ch;
++i;
ch = _cin.get();
}
//如果i>0 即temp中还有数据,将其转存即可
if (i > 0)
{
temp[i] = '\0';
s += temp;
}
return _cin;
}
源码概览
#include<iostream>
#include<cassert>
using namespace std;
namespace qqq
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
public:
string(const char* str = "")
{
assert(str);
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
memcpy(_str, str, _size + 1);
}
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
memcpy(_str, s._str, s._size + 1);
}
//string& operator=(const string& s); //老版本
string& operator=(string s)
{
swap(s);
return *this;
}
~string()
{
_size = 0;
_capacity = 0;
delete[] _str;
}
//
// iterator
string::iterator begin()
{
return _str;
}
string::iterator end()
{
return _str + _size;
}
string::const_iterator begin() const
{
return _str;
}
string::const_iterator end() const
{
return _str + _size;
}
/
// modify
// 在pos位置上插入字符c/字符串str,并返回
string& insert(size_t pos, char c)
{
assert(pos <= _size);
if (_size == _capacity)
{
if (_capacity != 0)
{
reserve(2 * _capacity);
}
else
{
reserve(4);
}
}
size_t end = _size + 1;
while (end > pos) //pos为size_t,当end与pos比较时,会转化为size_t而永远不可能小于0.故将end+1,后给前赋值
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = c;
++_size;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
reserve(len + _size);
size_t end = _size + len;
while (end > pos + len - 1) //写成end >= pos+len就会有问题(在0位置插一个"")
{
_str[end] = _str[end - len];
--end;
}
for (size_t i = 0; i < len; ++i)
{
_str[pos + i] = str[i];
}
_size += len;
return *this;
}
// 删除pos位置上的元素,并返回
string& erase(size_t pos, size_t len)
{
if (len == npos)
{
_size = pos;
_str[_size] = '\0';
}
for (size_t i = 0; i < _size - pos; ++i)
{
_str[pos + i] = _str[pos + len + i];
}
_size -= len;
return *this;
}
//尾插
void push_back(char c)
{
if (_size == _capacity)
{
if (_capacity != 0)
{
reserve(2 * _capacity);
}
else
{
reserve(4);
}
}
_str[_size] = c;
++_size;
_str[_size] = '\0';
}
string& operator+=(char c)
{
push_back(c);
return *this;
}
//尾追加
void append(const char* str)
{
assert(str);
size_t newlenth = _size + strlen(str);
if (newlenth > _capacity)
{
reserve(newlenth);
}
strcpy(_str + _size, str);
_size = newlenth;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
void clear()
{
_size = 0;
_str[_size] = '\0';
}
void swap(string& s)
{
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
std::swap(_str, s._str);
}
const char* c_str() const
{
return _str;
}
/
// capacity
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
bool empty() const
{
if (_size == 0)
{
return true;
}
return false;
}
void resize(size_t n, char c)
{
if (n > _size)
{
if (n > _capacity)
{
reserve(n + _size);
}
for (size_t i = _size; i < n; ++i)
{
_str[i] = c; //这里的[]是访问数组元素,并非运算符重载的调用,所以不会越界
}
_size = n;
_str[_size] = '\0';
}
else
{
_size = n;
_str[_size] = '\0';
}
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* newstr = new char[n + 1]{ 0 };
memcpy(newstr, _str, _size + 1);
//strcpy(newstr, _str);
delete[] _str;
_str = newstr;
_capacity = n;
}
}
/
// access
char& operator[](size_t index)
{
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index) const
{
assert(index < _size);
return _str[index];
}
/
//relational operators
bool operator<(const string& s) const
{
for (size_t i = 0; i < (_size < s._size ? _size : s._size); ++i)
{
if (_str[i] < s._str[i])
return true;
if (_str[i] > s._str[i])
return false;
}
if (_size < s._size)
return true;
else
return false;
}
bool operator==(const string& s) const
{
if (_size != s._size)
return false;
for (size_t i = 0; i < _size; ++i)
{
if (_str[i] != s._str[i])
return false;
}
return true;
}
bool operator<=(const string& s) const
{
if (*this < s || *this == s)
return true;
else
return false;
}
bool operator>(const string& s) const
{
if (!(*this <= s))
return true;
else
return false;
}
bool operator>=(const string& s) const
{
if (!(*this < s))
return true;
else
return false;
}
bool operator!=(const string& s) const
{
if (!(*this == s))
return true;
else
return false;
}
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos) const
{
assert(s);
assert(pos < _size);
char* pchar = strstr(_str, s);
if (pchar == nullptr)
{
return npos;
}
if ((size_t)(pchar - _str) >= pos)
{
return pchar - _str;
}
return npos;
}
private:
char* _str;
size_t _capacity;
size_t _size;
const static size_t npos;
};
const size_t string::npos = -1;
ostream& operator<<(ostream& _cout, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
_cout << s[i];
}
return _cout;
}
istream& operator>>(istream& _cin, string& s)
{
s.clear();
char ch = _cin.get();
//清除缓冲区中的空格与换行
while (ch == ' ' || ch == '\n')
{
ch = _cin.get();
}
//定义一个128的字符数组
char temp[128] = { 0 };
int i = 0;
while (ch != ' ' && ch != '\n')
{
if (i == 127)
{
s += temp;
i = 0;
}
temp[i] = ch;
++i;
ch = _cin.get();
}
//如果i>0 即temp中还有数据,将其转存即可
if (i > 0)
{
temp[i] = '\0';
s += temp;
}
return _cin;
}
}
总结
到此,关于string类的模拟实现就介绍完了
相信通过模拟实现string类可以使我们更深入地理解string
如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出
如果本文对你有帮助,希望一键三连哦
希望与大家共同进步哦