在上一篇文章中,我们详细介绍了string类一些常用接口的使用,那这篇文章,我们将对string进行一个模拟实现,帮助大家理解的更加深刻。
在上篇文章中我们了解:
string的底层其实就是一个支持动态增长的字符数组。那确定它的结构,接下来我们就开始模拟实现它。
首先新建一个头文件string.h
,定义一个string类:
class string
{
public :
//成员函数
private :
char* _str;
size_t _size;
size_t _capacity;
};
这里string类的三个成员变量,一个字符指针
_str
指向开辟的动态数组,_size
标识有效数据个数,_capacity
记录容量的大小(不包含’\0’)。
但是因为标准库里已经有string类,为了避免冲突,我们需要定义一个命名空间,把我们自己实现的string类放到自己的命名空间里面。
namespace w
{
class string
{
public :
//成员函数
private :
char* _str;
size_t _size;
size_t _capacity;
};
}
首先我们来模拟实现一个带参构造函数:
在之前的文章中我们提到尽量使用初始化列表进行初始化,我们可以这样写:
但是这里你会发现程序报错了,因为如果像上图一样初始化,首先涉及到权限放大的问题(之前文章有讲过)char* str
被const
修饰,不能被修改,但是赋给_str
,_str
是char*
类型的,可以修改。其次用常量字符串去初始化也不能被修改。
那怎么办呢? 我们这里不直接传参而是开空间,用strcpy去进行拷贝:
string(const char* str)
:_str(new char[strlen(str)+1])
,_size(strlen(str))
,_capacity(strlen(str))
{
strcpy(_str, str);
}
顺便这里我们提供一个接口用来返回字符串:
const char* c_str()
{
return _str;
}
这里我们直接顺便给出析构函数:
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
那这样真的可行吗?
如果这里_str
传空指针那么在刚刚实现的c_str
函数就会返回空程序,程序会崩溃。并且在标准库里的c_str
接口即使传空也是会有返回值的。
那这里应该怎么办呢? 我们可以这样写:
string()
:_str(new char[1])
,_size(0)
,_capacity(0)
{
_str[0] = '\0';
}
这里我们给
_str
开辟一个空间,然后给这块空间给上'\0'
。这样就不会出现上面的问题了。
我们之前讲过无参和带参的可以用全缺省。
我们来看几种写法:
能这样写吗?答案是肯定不能这样写类型就不匹配,一个是字符一个是字符串。
能这样写吗? 答案是肯定不能。这样写strlen里的str
就是空串了。
我们知道在标准库中可以通过下标去访问字符串中的某一个字符,下面我们来实现对
[]
的重载。
接下来我们来实现一下
[]
的重载:
这里我们实现了两个版本普通版本对应普通对象,const版本对应const对象,且这两个函数构成函数重载。
除了
[]
可以遍历访问string对象,我们还可以用迭代器进行访问。
那迭代器我们说了大家可以理解成一个像指针一样的东西,但是不一定是指针。
我们最开始介绍了STL有好几个版本,不同的版本实现可能是不一样的。
那其实vs下string的迭代器呢就不是使用指针实现的,而G++下使用的SGI版本是指针实现的。
那这里我们模拟实现就使用指针来实现:
同样的我们还可以使用范围for进行遍历:
范围for的底层就是用的迭代器。
大家可以理解成范围for的语法其实就跟我们之前学过的宏有点类似,它会被替换成迭代器,相当于把*it赋值给ch。范围for的底层就是无脑替换。
首先我们来实现一下
push_back()
和append()
.这两个都是插入数据,既然插入数据那我们就必须考虑扩容的问题。
那这里如果扩容的话,我们一次扩多少呢?
对于push_back来说一次扩二倍没问题,但是append一次扩二倍有可能是不行的。
为什么?
如果当前的容量是10,现在追加一个长度为25的字符串,扩容到原来的两倍才
20,也是不够用的。
那这里我们通过string的另一个接口reserve,它可以改变容量为我们指定的大小,帮助我们扩容。
下面我们就先来实现一下reserve。
我们先来看一下reserve怎么实现:
这里当参数n的值小于_capacity
,如果不加这个if判断这里就会缩容。但是我们知道,库里的接口是不会缩容的。所以需要加上这个条件判断。
那接下来有了
reserve
我们继续来实现push_back
和append
。
我们虽然有push_back和append但是我们更喜欢用重载的
+=
。当然+=的底层也是可以用push_back和append实现的。
首先我们来实现一下在pos位置插入n个字符:
逻辑其实是比较简单的。首先判断一下,是否需要扩容,然后就插入数据,如果往中间插就需要挪动数据。
程序这里挂了。那为什么呢?
这里当pos = 0
时,end等于0时还会进入循环,end再- -会变成多少? 是-1吗?
这里end的类型是szie_t,无符号整型,所以end为0后再- -并不是-1,而是整型最大值,发生越界,循环也没正常结束,所以程序崩了。
那怎么解决呢?把end改成int可行吗?
这里也是不可行的。end和pos比较,end变成int,但是pos是size_t类型,这里是会发生整型提升(C语言知识)。那我们应该如何解决呢?
刚才是插入一个字符,现在我们再来实现插入字符串的。那么逻辑和上面其实是一样的。只不过上面我们只需要挪出n个空间就可以了,那这里我们需要挪动数据腾出
strlen(str)
个空间。
对于
erase
首先第一种情况就是pos+len
小于字符串的长度,那我们需要把pos位置开始的后len个字符删掉,但是仍然保留后续字符。那这里就是挪动后面的数据,把需要删除的覆盖掉就行。
那其它情况就是len比较大,pos+len直接大于等于字符串的长度,那就把pos后面的全部删掉。或者没有传pos这个参数,缺省值npos,那也要把后面的全删,所以这两种情况可以统一处理。这里只需要把pos位置给成“\0”
就行了。
下面我们再来实现一下
substr
。它的逻辑也是很简单的。
这里程序出错发生了一个经典的浅拷贝的问题。在之前的文章中我们也有讲过若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数拷贝对象 按内存存储字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝,就会出现问题。
#include
using namespace std;
namespace w
{
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 = "")
:_str(new char[strlen(str)+1])
,_size(strlen(str))
,_capacity(strlen(str))
{
strcpy(_str, str);
}
string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
const char* c_str() const
{
return _str;
}
size_t size() 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];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
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);
}
strcpy(_str + _size, str);
_size += len;
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
void insert(size_t pos, size_t n, char ch)
{
assert(pos <= _size);
if (_size +n > _capacity)
{
// 至少扩容到_size + len
reserve(_size + n);
}
// 添加注释最好
size_t end = _size;
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;
}
void insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
// 至少扩容到_size + len
reserve(_size + len);
}
// 添加注释最好
size_t end = _size;
while (end >= pos && end != npos)
{
_str[end + len] = _str[end];
--end;
}
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
string& erase(size_t pos, size_t len = npos)
{
assert(pos <= _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
_str[_size] = '\0';
}
else
{
size_t end = pos + len;
while (end <= _size)
{
_str[pos++] = _str[end++];
}
_size -= len;
}
return *this;
}
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;
}
}
return npos;
}
size_t find(const char* str , size_t pos = 0)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr)
{
return ptr - _str;
}
else
{
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)
{
n = _size - pos;
}
string tmp;
tmp.reserve(n);
for (size_t i = pos; i < pos + n; i++)
{
tmp += _str[i];
}
return tmp;
}
private :
char* _str;
size_t _size;
size_t _capacity;
public:
const static size_t npos;
};
const size_t string::npos = -1;
}
#include "Mystring.h"
void test_string1()
{
w ::string s1("hello world");
cout << s1.c_str() << endl;
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i] << " ";
}
cout << endl;
w ::string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout <<endl;
for (auto ch : s1)
{
cout << ch <<" ";
}
cout <<endl;
}
void test_string2()
{
w::string s1("hello world");
cout << s1.c_str() << endl;
s1.push_back(' ');
s1.push_back('#');
s1.append("hello");
cout << s1.c_str() << endl;
w::string s2("hello world");
cout << s2.c_str() << endl;
s2 += ' ';
s2 += '#';
s2 += "hello code";
cout << s2.c_str() << endl;
}
void test_string3()
{
w::string s1("helloworld");
cout << s1.c_str() << endl;
s1.insert(5, 3, '#');
cout << s1.c_str() << endl;
s1.insert(0, 3, '#');
cout << s1.c_str() << endl;
w::string s2("helloworld");
s2.insert(5, "%%%%%");
cout << s2.c_str() << endl;
}
void test_string4()
{
w::string s1("helloworld");
cout << s1.c_str() << endl;
s1.erase(5, 3);
cout << s1.c_str() << endl;
s1.erase(5, 30);
cout << s1.c_str() << endl;
s1.erase(2);
cout << s1.c_str() << endl;
}
void test_string5()
{
w::string s1("helloworld");
cout << s1.find('w',2) << endl;
}
void test_string6()
{
w::string s1("hello world");
w::string s2(s1);
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
}
int main()
{
test_string6();
return 0;
}
文章篇幅有限,剩余内容将在下篇进行讲解。