一、引言
在之前的文章中,我们一同学习了有关类和对象、模板、动态内存管理的相关知识,那么接下来一段时间我们将要趁热打铁,一起来手撕C++库中最重要的一个库----STL中的一些容器,在手撕它们之前,我将先介绍一下对应的容器,在这个过程中,我们将加深之前学习过的相关知识。
二、string的前置内容
1、string的介绍
事实上string并不是STL中的一个容器,这是由于C++的发展历史的原因,但是随着语言的发展,string与STL中的容器的风格以及使用方法已经有着很高的耦合度了,所以我将它放在STL这里一起学习。
string是一个字符串类,可以借助其提供的接口进行字符串的各种增删查改操作,它的底层其实就是一个存储字符数组的顺序表。
· 2、string中的常用接口
事实上,string所提供的接口是有很多冗余的,大概有100多个接口,在实现部分都只涉及到一些常用的接口,通过这些常用的接口已经可以处理平常所能遇到的几乎所有情况了,如果需要了解所有接口或者对于以下接口的使用还不太熟悉的话,可以调转到以下网站了解:cplusplus.com - The C++ Resources Networkhttps://legacy.cplusplus.com/三、手撕一个string类
1、string类的成员变量及整体的框架
为了与库中的string区分,我们将在自己的命名空间中进行定义相关变量,这个命名空间的名字是无所谓的,为了方便起见,我将直接展开std命名空间,但是在真实场景下,不直接展开会更好一些
同时在这里说明一下,我们将在写一个函数之前先将库中的函数头放在前面,以其为导引,同时在这里简单的说明一下该函数的作用
string类是一个字符类型的顺序表,所以需要一个内置的字符类型的指针来管理字符串中的内容,对于一个顺序表,需要记录它的长度,同时我们将动态开辟空间,所以记录此时string中的空间是多少也是非常重要的:
涉及到类似顺序表的结构,_size、_capacity都是必要的,这个在以后也经常用到,同时在命名时,在成员变量之前加一个‘_’可以区分成员变量与外部变量
2、类的构造函数与析构函数
库中的函数头:
以上是库中所提供的所有的构造函数,我们只实现图中圈出的两个,还有几个是拷贝构造函数,在后面会实现,同时由于析构函数名一定并且没有返回值,不传参,这里就不展示了
一般来说构造函数我们更倾向于在初始化列表进行初始化,但是由于要开空间,同时对于一个内置类型来说,在函数体内赋值和在初始化列表进行初始化的效率差别不是很大,所以我都采用函数体内赋值:
可以看到,在上面我们只实现了一个构造函数但是由于它是带缺省值的,如果不传参默认会构造一个空串,所以与库中的函数是配合的,需要注意的是,在开空间时一定要多开一个空间,这个位置是留给‘\0’的
3、类中留给类外读取信息的几个简单函数
库中的函数头:
(1).size函数:返回此时字符串中的元素个数
(2).c_str函数:返回一个c风格的字符串
这两个函数实现起来是很简单的,就不做过多的介绍了:
4、[]操作符重载
库中的函数头:
[]操作符重载的作用是让我们实现的类类型可以像正常的数组一样使用[]访问下标的方式来访问字符串中指定位置的内容,使用起来很方便,同时代码的可读性变高
可以看到,该操作重载有两个函数重载,这两个函数重载都是很有必要的,当this指针不被const修饰时,函数提供给非const类型使用,可读可写;当this指针被const修饰时,函数提供给const类型使用,只读不写:
可以看到,该函数的实现也是非常的方便的,并且两个重载的内容也一模一样,这时候我们可以使用模板来简化代码,但由于这是很简单的两个重载,所以没有必要,在以后STL中其它部分,我们会大量的使用模板的相关知识
5、完成迭代器的封装
库中的函数头:
(1).begin
(2).end
迭代器是STL的相关容器中的一个重要工具,常被用来遍历容器,它是类中定义的一个类型,使用起来和指针非常相似,在类中定义类型有两种方式,封装内部类和typedef,很明显使用指针就可以直接遍历一个字符类型的数组,所以string类的迭代器我们可以直接使用typedef封装字符指针的方式
了解迭代器之后,我们还需要了解begin和end是留给用户使用迭代器的两个接口,begin返回指向首元素的迭代器,end返回指向末尾元素下一个位置的迭代器,它们也都进行了重载,同样分为只读不写和可读可写,在之前已经详细介绍过,这里就不多介绍了:
6、一个特殊的常量
库中的介绍:
在库中定义了一个静态成员变量npos,它的值是-1,但由于它是size_t类型的,所以实质上是一个非常大的正整数,我们默认一个自负床不会有这么长,所以在库中的函数将npos这一静态成员变量当作无穷大来使用:
在这里需要注意:类中的静态成员变量一定要记得在类外进行定义,虽然const修饰的静态成员变量是可以i直接在类内声明时直接定义的,但为了统一性,这里我还是在类外进行了定义
7、拷贝构造函数
库中的函数头:
拷贝构造函数的作用在这里就不多介绍了,它在之后的函数返回一个string类型、传参时传一个string类型、赋值运算符重载时都会用到:
8、开空间函数
库中的函数头:
reserve:开指定大小的空间,将原数据进行拷贝,剩余部分不做处理
9、插入相关函数-----append
库中的函数头:
append函数是用于在字符串末尾插入的函数,可以看到库中提供了很多的函数重载,接下来我们只会实现以上圈出的几个,同时我们将借助缺省值简化代码:
纠正一下:不小心忘记进行:_size += n\_size += sublen啦,这是在写完之后测试的时候发现的,所以一定要边写边测试啊!!!
10、交换函数
库中的函数头:
swap函数顾名思义就是交换两个string类对象,需要注意的是我们将完成深拷贝,也就是说要重新开空间,而不能简单的交换两个类对象的成员变量,这时候我们可以借助算法库中提供的swap函数,它可以帮助我们完成深拷贝,事实上我们可以直接进行深拷贝,但是借助算法库实现会更简单:
需要注意的是:在这里我们需要指定标准命名空间
11、赋值运算符重载
库中的函数头:
库中海还有该函数的其它重载,但其实都可以通过该函数完成剩余操作,所以只实现这一个,同时,由于我们可以借助一种更简单的方式来完成该函数的实现,所以接下来我对该函数的实现时,函数头与库中的略有差异,但是对于用户的使用是相同的:
对以上代码做一下解释:我们的函数实参传给形参会进行临时拷贝,这是会调用我们之前实现过的拷贝构造函数进行深拷贝,再交换*this与str,*this就变成了str,str是形参,出作用域销毁,调用析构函数,不会造成内存泄漏,*this出函数还存在,进行传引用返回
12、其它运算符重载
由于接下来都是正常的运算符重载,功能也是显而易见的,所以在这里就不提供库中的函数头了,直接进行相关的实现:
//其它运算符重载
//+=运算符重载
string& operator+=(const string& str)
{
return append(str);
}
string& operator+=(const char* s)
{
return append(s);
}
string& operator+=(char c)
{
return append(1, c);
}
//+运算符重载
string operator+(const string& str)
{
string strtmp(*this);
return (strtmp += str);
}
string operator+(const char* s)
{
string strtmp(*this);
return (strtmp += s);
}
string operator+(char c)
{
string strtmp(*this);
return (strtmp += c);
}
//<运算符重载
bool operator < (const string& s) const
{
int minlen = _size;
if (s._size < _size)
{
minlen = s._size;
}
for (int i = 0; i < minlen; i++)
{
if (_str[i] != s._str[i])
{
return _str[i] < s._str[i];
}
}
return _size < s._size;
}
//==运算符重载
bool operator==(const string& s) const
{
if (_size != s._size) return false;
for (int i = 0; i < _size; i++)
{
if (_str[i] != s._str[i])
{
return false;
}
}
return true;
}
//<=运算符重载
bool operator<=(const string& s) const
{
return ((*this < s) || (*this == s));
}
//>运算符重载
bool operator>(const string& s) const
{
return (!(*this < s) && !(*this == s));
}
//>=运算符重载
bool operator>=(const string& s) const
{
return ((*this > s) || (*this == s));
}
//!=运算符重载
bool operator!=(const string& s) const
{
return !(*this == s);
}
以上就是所有可能用到的运算符重载了,由于很长,所以我将代码直接放在上面,可以看到,由于这些逻辑运算符有天然的互斥性,所以我们可以很好的进行代码复用,大大提高了代码的质量
13、插入相关函数-----push_back
库中的函数头:
该函数的作用是在字符串末尾插入一个字符,无返回值:
在这个位置我们可以直接进行代码复用
14、插入相关函数-----insert
库中的函数头:
库中对于插入函数也是提供了非常多的重载,我们只实现图中圈出的几个,同时我们会使用缺省值的方式简化代码:
//插入函数
string& insert(size_t pos, const string& str, size_t subpos = 0, size_t sublen = npos)
{
assert(pos < _size);
assert(subpos < str._size);
if (subpos + sublen >= str._size) sublen = str._size - subpos;
if (_capacity < _size + sublen + 1)
{
reserve(_size + sublen + 1 + 0.5 * sublen);
}
int end = _size , next = end + sublen;
while (next > pos)
{
_str[next--] = _str[end--];
}
for (int i = 0; i < sublen; i++)
{
_str[pos + i] = str._str[i];
}
_size += sublen;
return *this;
}
string& insert(size_t pos, const char* s, size_t n = npos)
{
string strtmp(s);
return insert(pos, s, 0, n);
}
string& insert(size_t pos, size_t n, char c)
{
assert(pos < _size);
if (_capacity < _size + n + 1)
{
reserve(_size + n + 1 + 0.5 * n);
}
int end = _size, next = end + n;
while (next > pos)
{
_str[next] = _str[end];
}
for (int i = 0; i < n; i++)
{
_str[pos + n] = c;
}
_size += n;
return *this;
}
可以看到,在上面我们也进行了一定程度的代码复用,所以代码复用是非常重要的,可以大大提高代码质量
15、删除函数
库中的函数头:
erase函数的作用是从pos位置开始,删除len长度的字符:
16、查找相关函数
库中的函数头:
库中对于查找函数也是实现了多个函数重载,在这里我们只实现上面圈出的两个,其他的通过借助这两个函数也是都可以完成的:
在上面的实现中我们借助了C语言常用的几个函数,简化了代码
17、返回子串的函数
库中的函数头:
该函数的作用是返回从pos位置开始len长度的子串:
18、清理函数
库中的函数头:
该函数可以清空字符串中的数据:
四、最终的string类
终于,我们一起写完了一个简陋版的string类,需要注意的是在这个过程中我们更关注的是了解string的使用、加深对于类和对象等知识的理解和提升我们的代码能力,并不是写出一个更好的string类,最终完成的string类如下:
namespace bea
{
class string
{
public:
//相关函数
//构造函数
string(const char* s = "")
{
int n = strlen(s);
_str = new char[n + 1];
memcpy(_str, s, n + 1);
_size = n;
_capacity = n;
}
//size函数
size_t size() const
{
return _size;
}
//c_str函数
const char* c_str() const
{
return _str;
}
//[]操作符重载
//
// 普通类型
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];
}
//迭代器相关接口
//先进行类型的封装
typedef char* iterator;
typedef const char* const_iterator;
//begin函数
iterator begin()
{
return _str;
}
const_iterator begin() const
{
return _str;
}
//end函数
iterator end()
{
return _str + _size;
}
const_iterator end() const
{
return _str + _size;
}
string(const string& str)
{
_str = new char[str._capacity + 1];
memcpy(_str, str._str, _size + 1);
_size = str._size;
_capacity = str._capacity;
}
//开空间函数
void reserve(size_t n = 0)
{
if (n > _capacity)
{
char* strtmp = new char[n + 1];
memcpy(strtmp, _str, _size + 1);
delete[] _str;
_str = strtmp;
_capacity = n;
}
}
//尾接函数
//尾接C风格字符串
//接入s字符串的前n个字符
string& append(const char* s, size_t n = npos)
{
size_t len = strlen(s);
if (n > len) n = len;
if (_capacity < _size + n + 1)
{
reserve(_size + n + 1 + 0.5 * n);
}
for (int i = 0; i < n; i++)
{
_str[_size + i] = s[i];
}
_str[_size + n + 1] = '\0';
_size += n;
return *this;
}
//尾接一个string
//接入str的subpos开始sublen长度的子串
string& append(const string& str, size_t subpos = 0, size_t sublen = npos)
{
assert(subpos < str._size);
if (subpos + sublen >= str._size) sublen = _size - subpos;
if (_capacity < _size + sublen + 1)
{
reserve(_size + sublen + 1 + 0.5 * sublen);
}
for (int i = 0; i < sublen; i++)
{
_str[_size + i] = str._str[i];
}
_str[_size + sublen + 1] = '\0';
_size += sublen;
return *this;
}
//尾接n个ch
string& append(size_t n, char ch)
{
if (_capacity < _size + n + 1)
{
reserve(_size + n + n * 0.5);
}
for (int i = 0; i < n; i++)
{
_str[_size + i] = ch;
}
_str[_size + n + 1] = '\0';
_size += n;
return *this;
}
//交换函数
void swap(string& str)
{
std::swap(_str, str._str);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
}
//赋值运算符重载
string& operator=(string str)
{
swap(str);
return *this;
}
//其它运算符重载
//+=运算符重载
string& operator+=(const string& str)
{
return append(str);
}
string& operator+=(const char* s)
{
return append(s);
}
string& operator+=(char c)
{
return append(1, c);
}
//+运算符重载
string operator+(const string& str)
{
string strtmp(*this);
return (strtmp += str);
}
string operator+(const char* s)
{
string strtmp(*this);
return (strtmp += s);
}
string operator+(char c)
{
string strtmp(*this);
return (strtmp += c);
}
//<运算符重载
bool operator < (const string& s) const
{
int minlen = _size;
if (s._size < _size)
{
minlen = s._size;
}
for (int i = 0; i < minlen; i++)
{
if (_str[i] != s._str[i])
{
return _str[i] < s._str[i];
}
}
return _size < s._size;
}
//==运算符重载
bool operator==(const string& s) const
{
if (_size != s._size) return false;
for (int i = 0; i < _size; i++)
{
if (_str[i] != s._str[i])
{
return false;
}
}
return true;
}
//<=运算符重载
bool operator<=(const string& s) const
{
return ((*this < s) || (*this == s));
}
//>运算符重载
bool operator>(const string& s) const
{
return (!(*this < s) && !(*this == s));
}
//>=运算符重载
bool operator>=(const string& s) const
{
return ((*this > s) || (*this == s));
}
//!=运算符重载
bool operator!=(const string& s) const
{
return !(*this == s);
}
//push_back函数
void push_back(char c)
{
*this += c;
}
//插入函数
string& insert(size_t pos, const string& str, size_t subpos = 0, size_t sublen = npos)
{
assert(pos < _size);
assert(subpos < str._size);
if (subpos + sublen >= str._size) sublen = str._size - subpos;
if (_capacity < _size + sublen + 1)
{
reserve(_size + sublen + 1 + 0.5 * sublen);
}
int end = _size , next = end + sublen;
while (next > pos)
{
_str[next--] = _str[end--];
}
for (int i = 0; i < sublen; i++)
{
_str[pos + i] = str._str[i];
}
_str[pos + sublen + 1] = '\0';
_size += sublen;
return *this;
}
string& insert(size_t pos, const char* s, size_t n = npos)
{
string strtmp(s);
return insert(pos, s, 0, n);
}
string& insert(size_t pos, size_t n, char c)
{
assert(pos < _size);
if (_capacity < _size + n + 1)
{
reserve(_size + n + 1 + 0.5 * n);
}
int end = _size, next = end + n;
while (next > pos)
{
_str[next] = _str[end];
}
for (int i = 0; i < n; i++)
{
_str[pos + n] = c;
}
_str[pos + n + 1] = '\0';
_size += n;
return *this;
}
//删除函数
string& erase(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
if (pos + len >= _size)
{
_str[pos] = '\0';
_size -= len;
return *this;
}
int end = pos + len, front = pos;
while (front < _size)
{
_str[front++] = _str[end++];
}
_size -= len;
_str[pos + len + 1] = '\0';
return *this;
}
//找字符函数
size_t find(char ch, size_t pos = 0)
{
assert(pos < _size);
for (int 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)
{
if (pos == 0 && len >= _size) return *this;
if (pos + len >= _size) len = _size - pos;
string tmp;
for (int i = pos; i < pos + len; i++) tmp.push_back(_str[i]);
return tmp;
}
//清除函数
void clear()
{
_str[0] = '\0';
_size = 0;
}
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos;
};
const size_t string::npos = -1;
}
五、结语
以上就是本期关于手撕string类的所有内容了,感谢大家的阅读,欢迎各位于晏、亦菲和我一起交流、学习、进步!!!