string类的成员变量:
我们今天主要模拟实现下string类的一些常用函数接口,来进一步的理解string类。我们首先需要一个动态开辟的指针指向这个字符串,然后还需要容量和存储的个数,并且我们不能和标准库的string进行冲突所以我们需要写在我们自己的类域中,并且我们库中还有一个静态的变量是npos,就是无符号的-1,代表整形的最大值:
namespace huyang
{
class string
{
public:
//成员函数
private:
char *_str;
size_t size;
size_t capaticy;
public:
const static size_t npos = -1;
};
};
这其中有一点是非常奇怪的,首先我们的static成员变量应该在类中声明,在类外部定义,但是在类的内部如果是上面的写法那么就相当于是直接定义了,这点是C++的特例。因为我们的const 静态变量 npos后面需要访问,所以我们设置为共有。
string的构造函数:
我们的构造函数其中有很多的细节,我们慢慢来讲解:一般的我们指针初始化时,我们都是给nullptr初始化,但是我们的string是不可以的,因为我们有一个c_str函数的接口要提供,我们如果给成nullptr后,调用这个函数就会崩溃。根本原因就是我们默认的_str给的是空指针,我们调用c_str后会直接解引用访问它,导致崩溃
我们的无参的函数应该也开辟一个空间,来存放 '\0' ,并且我们使用new [] 进行开辟空间。正常我们应该使用new 就可以,但是为了后面的析构函数对应,那么我们应该使用new []来和delete [] 配合使用。
//无参的构造
string()
:_str(new char[1])
, _size(0)
, _capaticy(0)
{
_str[0] = '\0';
}
如果有参数的构造呢?我们应该先计算出字符串长度,然后我们在开辟空间,开辟空间时,我们要多开一个字节用来存放 ' \0 ',接着把size和capaticy共同的赋初值,最后把这个字符串拷贝到我们开辟好的空间上。string(const char* str) :_str(new char[strlen(str) + 1]) , _size(strlen(str)) , _capaticy(strlen(str)) { strcpy(_str, str); }
这个代码我们还可以进行优化,因为我们只用计算出一次strlen就可以啦,比如我们size是用strlen计算得出的,那么我们开辟的空间就是size+1,capaticy就是size。这样我们减少了计算,毕竟我们的strlen接口是O(N)的接口。
//error 错误的示范
string(const char* str)
:_str(new char[_capaticy + 1])
, _size(_capaticy)
, _capaticy(strlen(str))
{
strcpy(_str, str);
}
但是这样我们还要有点需要注意的,那就是初始化链表的顺序,这个初始化的顺序和我们写的顺序无关,和我们的声明顺序有关的,就是我们是先声明的就先初始化,后声明就后初始化,我们是先声明的_str,再声明的size,最后声明的capaticy,那么我们如果是按照上面的逻辑就是下面的情况:我们这其中有5个有效字符了,但是我们的size还是为0,原因就是我们的成员变量之间是有关系的,我们在初始化时先初始化的是_str,_str是用一个capaticy的随机值来进行初始化的,而我们的size则是使用_capaticy初始化的,最后容量才是我们的strlen的值,这样是不合理的,我们的解决办法就是不使用初始化列表,而在函数体内初始化,函数体就不会出现谁先初始化导致的错误。
//带参数的构造
string(const char* str)
{
_size = strlen(str);
_str = new char[_size + 1];
_capaticy = _size;
strcpy(_str, str);
}
全缺省的构造函数:
我们搞定了无参和有参数的,最后我们使用最多最好用的还是全缺省的。我们一般缺省的_str提供的是一个空串,size和capaticy是0。
//全缺省的
string(const char* str = "")
{
_size = strlen(str);
_capaticy = _size;
_str = new char[_size + 1];
strcpy(_str, str);
}
string的拷贝构造,赋值(opeartor = ) 和析构:(深浅拷贝)
我们string类默认生成的拷贝构造是浅拷贝,但是我们的浅拷贝在析构是会出错,所以我们要实现出深拷贝。其中我们分为了传统写法和新派写法,我们分别来看看吧
关于拷贝构造的传统写法:
我们首先肯定是开辟一个和 s._capaticy 一样的空间,把size和capapticy设置为一样,然后把数据拷贝过来就好了。
//传统写法的深拷贝 string(const string& s) :_str(new char[s._capaticy + 1]) , _size(s.size()) , _capaticy(s._capaticy) { strcpy(_str, s._str); }
我们深拷贝的现代写法中更多的是复用构造函数的部分,减少拷贝直接交换。在写之前我们还要支持一下string的swap函数,我们swap函数中也是使用std标准库中提供的,所以我们需要在前面加上 ::类作用限定符,空白表示的是全局域。我们要使用一个临时对象进行构造,然后再把构造好的对象进行交换到我们引用传参的对象中。就完成了拷贝。
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capaticy, s._capaticy);
}
//现代写法的拷贝构造
string(const string& s)
:_str(nullptr)
, _size(0)
, _capaticy(0)
{
string tmp(s._str);
swap(tmp);
}
我们的赋值操作符也要进行深拷贝,也要进行就交换。赋值的传统写法:首先我们先判断是否自己和自己交换,如果是的话就直接返回,如果不是,我们首先要开辟空间,然后我们进行拷贝字符,这样其实防止了new出错之后破坏原来的字符串的情况,最后进行交换返回。
//传统写法赋值
string& operator=(const string& s)
{
if (this != &s)
{
char* tmp = new char[s._capaticy + 1];
strcpy(_str, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capaticy = s._capaticy;
}
return *this;
}
现代写法中我们也要先判断,接着我们直接进行构造一个对象,然后在交换。
//现代写法1.0
string& operator=(const string& s)
{
if (this != &s)
{
string(tmp);
swap(tmp);
}
}
然后我们对现代写法在优化下:我们直接使用传值传参,这个形参值也会进行拷贝,然后我们直接利用参数进行构造,在进行交换,这样也防止了我们会自己拷贝自己的情况。
//现代写法2.0
string& operator=(string s)
{
string(s);
return *this;
}
string的遍历方式:
我们在string中遍历字符串,一般无外乎以下三中方式:
1. 下标遍历( operator []) 2. 迭代器(iterator ) 3.范围for(底层还是迭代器)
我们先说下标遍历吧:(operator [])
我们的_str是一个指针,那么我们可以通过数组的方式来访问,只需要重载operator []即可。我们还是要重载两个版本的,因为普通变量和const变量的访问权限不一样。
//普通变量,可读可写
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//const变量,只读属性
char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
2.迭代器(iteator)
在string中,迭代器就是一个指针,只不过我们进行了封装,typedef一下就可以啦,同样我们也要实现两个版本的,const和非const的。
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
const_iterator begin()const
{
return _str;
}
iterator end()
{
return _str +_size;
}
const_iterator end() const
{
return _str +_size;
}
3. 我们范围for的底层就是迭代器,所以我们不用实现,只要实现了迭代器,那么我们就可以直接使用范围for,我们可以一会儿看下底层调用。
我们来看下例子:
void test_string2()
{
string s1("world");
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i] << " ";
}
cout << endl;
string::iterator it = s1.begin();
while (it != s1.end())
{
(*it)++;
cout << *it << " ";
it++;
}
cout << endl;
for (auto& e : s1)
{
e--;
cout << e << " ";
}
cout << endl;
}
我们看下范围for和迭代器:
string类的申请空间:
一般是我们原空间容量满了,需要申请空间扩容,我们的扩容函数还是要先申请空间,然后在进行拷贝,接着我们delete原来的空间,把申请的空间的指针和 容量 赋值过去即可。
void reserve(size_t n)
{
if (n > _capaticy)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capaticy = n;
}
}
我们来实现下吧:
void resize(size_t n, char ch = '\0')//缺省参数给'\0'
{
if (n > _size)//如果比较大,那么我们申请空间,初始化
{
reserve(n);
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_str[n] = '\0';
_size = n;
}
else
{
_str[n] = '\0';
_size = n;
}
}
我们看下测试例子:
void test_string9()
{
string s0;
s0.resize(10);
cout << s0 << endl;
string s1;
s1.resize(16, 'x');
cout << s1 << endl;
string s2("11111111111111111111111111111111111111111111111111111111111111111");
s2.resize(10);
cout << s2 << endl;
}
第一行是10个空间被初始化为 ' \0 ',没有显示打印。
第二行是,我们传入的是16和 ' x ',所以就初始化为全 ' x '.
第三行虽然我们构造是传入的参数很长,但是我们resize时传入的是10,所以我们的只有前9个1,还有一个是 ' \0 '。
string的增删查改:
我们上面实现了扩容了,下面我们就实现下我们的增删查改吧。
push_back(char ch) 尾部插入字符,首先我们在扩容时,判断下string的对象是否是空串,接着就插入数据,++_size ,再把_size位置的值赋值为' \0 '。
void push_back(char ch)
{
if (_size == _capaticy)
{
//防止刚开始传的是空串。
reserve(_capaticy == 0 ? 4 : 2 * _capaticy);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';//一定要有'\0',这是C形式字符串结束的标志
}
append(const char* str) 这个是插入一个字符串,这个我们需要先提前开辟len字符的空间,然后我们进行拷贝,在把_size+=len。
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capaticy)
{
reserve(_size + len);
}
//从_str+_size位置插入字符串str
strcpy(_str + _size, str);
_size += len;
}
然后我们重载operator+=就可以直接复用上面的代码:
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
我们还要实现任意位置的插入字符或者字符串:
string& insert(size_t pos, char ch),对于这个函数我们首先要先进行断言,不能使pos超过_size,这个我们进行开辟空间,还要注意是否刚开始时空串这个问题,最后我那么进行挪动数据和放置数据,我们画图理解下这个挪动数据的过程。
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capaticy)
{
reserve(_capaticy == 0 ? 4 : 2 * _capaticy);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
代码如下:
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size+len > _capaticy)
{
reserve(_size + len);
}
size_t end = _size + len;
while (end >= pos+len)
{
_str[end] = _str[end-len];
--end;
}
strncpy(_str + len, str, len);
_size += len;
return *this;
}
我们实现了insert之后就可以把push_back和append进行修改,复用我们的insert就好了。
删除pos位置之后的n个字符:
void erase(size_t pos, size_t len = npos),我们先进行断言pos<_size,此时不能是 = size,因为_size处不可删除是 ' \0 ',接着我们判断下 pos + len是否大于size 或者 len是否是npos。如果是上面的两种情况那么直接把pos位置放入' \0 ',然后把_size = pos即可。
否则就是我们的pos+len小于size,那我们就把pos+len处的字符依次拷贝到pos后面即可。
代码如下:
void erase(size_t pos, size_t len = npos) { assert(pos < _size); if (len == pos || pos + len >_size) { _str[pos] = '\0'; _size = pos; } else { strcpy(_str + pos, _str + len + pos); _size -= len; } }
我们在实现下查找算法:size_t find(char ch, size_t pos = 0) const
从pos位置找一个字符,返回字符的下标。我们先断言pos<_size,再直接遍历就可以啦。
size_t find(char ch, size_t pos = 0) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t find(const char* sub, size_t pos = 0) const
在pos位置查找子串,然后我们返回最开始的下标。还是先断言,sub不能为空,并且下标要正常的pos<_size,我们可以使用strstr帮助我们实现。strstr找到时返回一个指针,我们获取下标直接让返回的指针减去_str即可。(因为我们是char类型)
size_t find(const char* sub, size_t pos = 0) const
{
assert(sub);
assert(pos < _size);
const char *ptr = strstr(_str + pos, sub);
if (ptr == nullptr)
{
return npos;
}
else
{
//返回的要是下标,ptr和_str是指针,指针相减就是下标(char类型的)
return ptr - _str;
}
}
我们还要提供一个函数获取pos位置后len个字符的函数,就是获取子串。
string substr(size_t pos, size_t len = 0)
和我们删除算法类似,首先我们断言pos<_size,如果len = pos 或者 len+pos >_size时,我们就从pos位置获取到最后即可。否则就是获取pos后len个字符组成子串。
string substr(size_t pos, size_t len = 0)
{
assert(pos < _size);
size_t realLen = len;
if (len == npos || pos + len >_size)
{
realLen = _size - pos;
}
string sub;
for (size_t i = 0; i < realLen; i++)
{
sub += _str[pos + i];
}
return sub;
}