一个类的构建首先要明确的就是类里面的成员函数,在string类中的结构是什么样的呢?
首先,string类是一个用char类型实例化的模板,类模板是basic_string,所以我们需要实现的是basic_string这个类模板,然后使用typedef basic_string
将实例化出来的类型重命名为string。由于其中对于其他编码方式的处理较为复杂,这里就不考虑,我们直接以string的方式实现basic_string
接下来要考虑的事情就是string中的成员变量,string类需要存放的是字符数组,结合之前在数据结构中学的动态顺序表的知识,所以需要一个指针用来管理数组,需要两个整型用来存放数据个数和容量,所以string类的主要结构如下:
class string
{
public:
//类中的各种成员函数
private:
char* _str;
size_t _size;
size_t _capacity;
};
构造函数分为无参和带参两种,对于无参的构造函数,默认构造空字符串“”,所以我们给个缺省值即可。
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
这里对于capacity的处理,我们选择只关注存放的有效字符个数,不算上\0
在之前的类和对象章节中,我们讲到,拷贝构造和赋值重载是默认成员函数,如果我们不显示写,编译器会自动生成。那么为什么对于string类,我们要写呢?
默认生成的拷贝构造对自定义类型调用其拷贝构造,对于内置类型完成值拷贝。如果使用默认生成的构造函数的话不能完成所需要的任务,而且会出现对同一块空间析构两次的问题。
如果执行浅拷贝,将会出现上图中的情况,两个对象指向了同一块空间,当其中任何一个对象析构之后,另一个对象就失效了,所以我们需要手动实现深拷贝
1.传统写法
针对我们的需求,我们很容易想到,按照构造一个对象的顺序一步一步来,先开辟一块大小相同的空间,然后将数据拷贝过去,再把_size和_capacity拷贝过去。这就是传统写法
string(const string& s)
{
_str = new T[s.capacity + 1];
strcpy(_str, s._str);
_size = s.size();
_capacity = s.capacity;
}
2.现代写法
在STL的发展中,总会有些人另辟蹊径,给出不同于常人的想法,正是这些脑洞大开的想法,让社会进步,下面,就让我们一起看看现代写法是什么样的
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
string tmp(s._str);
swap(tmp);
}
对于现代写法,我们可以这么理解,我们找了个替身工具人替我们完成任务,这个工具人就是tmp,他替我们完成了创建一个对象的任务,然后我们将tmp和this进行交换,那么在拷贝构造执行完毕之后,tmp对象将会自动调用析构函数,我们就不用管他了,此时this指向的对象中的内容就是与传入的s相同,也就是完成了深拷贝。
默认生成的赋值重载也是只执行值拷贝,所以对于string类,我们需要手动写赋值重载
1. 传统写法
对于赋值重载,我们需要实现的功能就是将this指向的类对象的值全部转换成传入的右操作数的值,按部就班的来做就是首先开辟一块空间,将s指向的数据依次拷贝到新空间中,然后释放掉原左操作数指向的空间,将其指向新空间,然后更新size和capacity。最后,其实需要进行一个判断,防止出现自己给自己赋值。
string& operator=(const string& s)
{
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
2. 现代写法1
和拷贝构造的现代写法类似,我们找一个工具人tmp来替我们构造出一个新的对象,然后与this执行交换,使得原this指向的对象在函数结束时被析构。
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s);
swap(tmp);
}
return *this;
}
3. 现代写法2
对于上述的现代写法,其实可以更加简化
string& operator=(string s)
{
swap(s);
return *this;
}
在传参的时候直接使用传值传参,在函数栈帧创建的时候构造一个对象s,这个s是传入的参数的拷贝,然后将this和这个s进行交换即可完成任务
注:在现代写法2中,我们发现好像没有进行自己给自己的赋值判断,这里说一下,使用这种写法,对于自己给自己赋值的情况,直接执行不会出现错误,只是效率有点低,但是这种场景出现的情况很少,所以可以不考虑。
析构函数就非常简单了,使用delete[]释放空间即可
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
对于string的迭代器,原生指针就能很好的支持迭代器行为,所以我们直接用原生指针。这里我们只实现正向迭代器的const和非const版本。
typedef char* iterator;
typedef const char* const_iterator;
iterator beign()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator beign() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
size和capacity直接返回对象中的成员变量即可
size_t size()
{
return _size;
}
size_t capacity()
{
return _capacity;
}
reserve是对string对象的容量进行操控的,当容量小于传入的值时,将会扩容,如果容量大于传入的值将不会做任何操作
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
resize的功能就是对string对象的size重新规划,当传入的n > capacity时,需要扩容,然后使用‘\0’来填充(默认情况下),当n小于元素个数时,将会把元素个数直接变成n,当n在size和capacity之间时,将会把后续的内容填充能‘\0’(默认情况下)
void reseize(size_t n, char ch = '\0')
{
if (n > _capacity)
{
reserve(n);
}
if (n > _size)
{
for (size_t i = _size; i < n; ++i)
{
_str[i] = ch;
}
}
_size = n;
_str[n] = '\0';
clear是清除所有数据,但是不销毁的函数,empty是判断是否为空的函数
void clear()
{
_size = 0;
_str[0] = '\0';
}
bool empty()
{
return _size == 0;
}
对于string的数据访问,我们一般有两种方式,第一就是通过迭代器(范围for也是迭代器的方式),第二种就是通过operator[],所以这里我们只实现一下operator[]
//非const版本,可读可写
char operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//const版本,可读不可写
const char operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
在学习数据结构的时候,我们一般学习的就是这种数据结构的增删查改四种操作,对于string类也是这样,所以在数据操作中,也分为增删查改四种。
1. push_back
尾插一个字符,在增加数据的时候,我们需要考虑是否需要扩容
void push_back(char ch)
{
if (_capacity == _size)
{
size_t newCapacity = _capacity == 0 ? 4 : 2 * _capacity;//扩容扩二倍
reserve(newCapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';//这里注意尾插之后要对字符串结尾的\0做一下补充
}
2. append
尾插一串字符(一个字符串),这里有很多重载,我们只实现一个C-string类型的。
void append(const char* str)
{
//这里需要判断一下需不需要扩容,如果需要的话要扩多大
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
3. operator+=
对于上述的两种插入方式,我们可以使用一个运算符重载全部解决,直接复用即可
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
4.insert
1. 插入字符
按照insert的逻辑,我们很容易的可以写出以下代码
string insert(size_t pos, char ch)
{
//判断位置是否合法
assert(pos <= _size);
//扩容
if (_size == _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newCapacity);
}
//挪动数据
size_t end = _size;
while (pos <= end)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
但是,如果当传入的pos==0时,将会出现死循环的问题,因为pos的类型是size_t,所以在进行比较的时候,编译器会进行隐式类型转换,把end转换成size_t的类型进行比较,所以如果传入的pos==0,就不可能出现pos > end的情况,也就是死循环了,那么我们要怎么解决这个问题呢?其实有两种解决方案
方案一:强转
将end定义成int类型,然后while中的判断把pos强转成int,这样就可以避免隐式类型转换,从而规避死循环的情况发生
int end = _size;
while ((int)pos <= end)
{
_str[end + 1] = _str[end];
--end;
}
但是这种方法看起来有点不太高级,所以这里我们有了另一种方法
方案二:将end指向\0后面的位置,这样就不会出现end<0的情况
size_t end = _size + 1;
while (pos < end)
{
_str[end] = _str[end - 1];
--end;
}
2. 插入字符串
插入字符串之前,我们同样要判断容量是否充足。这里在拷贝的过程中,我们要注意使用strcpy的时候,会把\0拷贝进去,所以我们不能使用strcpy而是使用strncpy。
string& insert(size_t pos, const char* str)
{
//判断位置是否合法
assert(pos <= _size);
size_t len = strlen(str);
//扩容
if (_capacity < len + _size)
{
reserve(len + _size);
}
//挪动数据
size_t end = _size + pos;
while (pos + len - 1 < end)
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
对于数据的删除,一般来说就是尾删和在任意位置删除。
1. pop_back
尾删很简单,我们只需要把最后一个有效字符的值替换成\0,然后更改_size即可
void pop_back()
{
_str[--_size] = '\0';
}
2. erase
erase的实现分为两种情况
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = 0;
}
//挪动数据
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
find:从pos位置开始查找字符或字符串,找到返回下标,找不到返回npos
1. 查找字符
size_t find(size_t pos, char ch)
{
assert(pos < _size);
for (size_t i = 0; i < _size; ++i)
{
if (_str[i] == ch)
return i;
}
return npos;
}
2. 查找字符串
对于字符串的查找我们可以借用strstr函数实现
size_t find(size_t pos, const char* str)
{
assert(pos < _size);
const char* ptr = strstr(_str, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
注:修改是建立在查找上的,所以这里就不专门讲解了
对于流插入和流提取的重载,我们在日期类的时候就已经讲过,不能重载成成员函数,如果重载成成员函数会导致this指针抢占第一个位置。所以需要重载成全局函数,然后在类里面设成友元。
1. 流插入
ostream& operator<<(ostream& _cout, const zht::string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
_cout << s[i];
}
return _cout;
}
2. 流提取
对于流提取,我们使用cin或者scanf是没有办法拿到空格和\0的,所以这里需要使用get函数来拿到
istream& operator>>(istream& _cin, zht::string& s)
{
s.clear();
char ch = _cin.get();
while (ch != ' ' && ch != '\0')
{
s += ch;
ch = _cin.get();
}
return _cin;
}
但是,对于上述的情况,如果需要扩容的话,会导致频繁扩容,所以我们在这里使用一个临时数组来存放,然后统一尾插到string中去。
istream& operator>>(istream& _cin, zht::string& s)
{
s.clear();
char buff[128] = { '\0' };
size_t i = 0;
char ch = _cin.get();
while (ch != ' ' && ch != '\0')
{
if (i == 127)
{
s += buff;
i = 0;
}
buff[i++] = ch;
ch = _cin.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return _cin;
}
在上文中,我们提到了一个swap函数,这个swap函数在使用的时候,形参只有一个,这个swap函数就是在string类中重载的一个函数。
为什么要在string里面重新重载一个swap函数呢?
这是因为算法库里面实现的swap函数是一个模板
可以看到,无论对于什么类型,他都会无脑的拷贝构造,这样其实对于string这种使用new在堆上开辟空间的类效率是不高的,所以需要在类内重新写一个swap函数,将类内的成员变量进行交换即可,这样就能省去new和delete的消耗。
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
2. c_str
对于Linux操作系统,它是使用C语言写的,所以它的系统接口也都是C形式的接口,而不支持string,所以我们在进行系统调用的时候,需要将string转换成C字符串的形式传参。c_str就是将string类返回成C字符串形式。
char* c_str()
{
return _str;
}
const char* c_str() const
{
return _str;
}
载一个swap函数呢?
这是因为算法库里面实现的swap函数是一个模板
[外链图片转存中…(img-CP2s20jF-1681724759328)]
可以看到,无论对于什么类型,他都会无脑的拷贝构造,这样其实对于string这种使用new在堆上开辟空间的类效率是不高的,所以需要在类内重新写一个swap函数,将类内的成员变量进行交换即可,这样就能省去new和delete的消耗。
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
2. c_str
对于Linux操作系统,它是使用C语言写的,所以它的系统接口也都是C形式的接口,而不支持string,所以我们在进行系统调用的时候,需要将string转换成C字符串的形式传参。c_str就是将string类返回成C字符串形式。
char* c_str()
{
return _str;
}
const char* c_str() const
{
return _str;
}