目录
一、引言
在上一篇博文中我们详细介绍了什么是STL?以及学习STL的重要性,同时还将string容器作为学习STL的开端,详细介绍了string容器的接口。学习一个容器最初步就是了解怎么使用,但这才是最开始,第二步也是最关键的一步就是我们要自己模式实现相应接口出来,亲手模拟实现核心接口才能真正理解其底层逻辑。今天就一起将string容器的相关重要接口模拟实现出来吧!
二、string类的默认函数实现
还记得我们学习类的方法吗?从成员变量开始,然后到简单的构造函数、析构函数等默认成员函数,最后再去根据需求看其他成员函数和非成员函数。
1.成员变量
#include<iostream>
#include<assert.h>
using namespace std;
namespace xiaoye
{
class string
{
private:
char* _str;//指向字符串
size_t _capacity;//容量大小
size_t _size;//字符串长度
static const size_t npos;//-1,表示"不存在"或"最大长度"
};
}
这里的成员变量包含一个指针指向字符串,一个无符号整型记录容量,还有一个无符号整型记录字符串长度,这里的npos是为了后面的缺省参数,默认为无符号的-1,也就相当于最大值。
2.构造函数
//默认构造
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
这里给普通构造函数一个缺省值变成了默认构造,也就不用写两个构造函数一个默认构造一个普通构造,这一个构造函数就可以实现功能。先计算了str字符串的长度,用到了strlen函数(计算字符串长度的函数),然后用new申请了一段空间,这里一定是_capacity+1是因为容量不包括‘\0’,但是要存储‘\0’,这个时候直接将原来的字符串拷贝给新申请的字符串空间之中去即可,用到了strcpy函数(字符串拷贝函数)。
3.析构函数
//析构函数
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
这里的析构函数一定是需要我们自己来实现,因为对于有资源空间申请的析构函数,不能直接使用编译器生成的默认析构函数,需要自己去释放申请的空间。我们用到了new[]去申请空间相应的就需要用delete[]去释放空间,然后将指针置为nullptr,容量和长度置为0。不过注意当_str为空指针的时候也就不需要我们去释放空间,因为为0也就是相当于没有申请空间。
4.拷贝构造函数
//普通的拷贝构造函数
string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
//交换函数
void swap(string& t)
{
std::swap(_str, t._str);
std::swap(_size, t._size);
std::swap(_capacity, t._capacity);
}
//现代写法
string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
对于有资源空间申请的拷贝构造函数也需要自己实现,因为编译器默认的拷贝构造函数是浅拷贝,在很多情况使用的时候会出问题。这里就需要自己实现,有两种实现的写法:第一种普通写法是先申请空间,然后直接将s的字符串、容量和长度拷贝过来即可。另外一种现代写法,通过直接用s的字符串进行构造函数建一个类对象,然后再将这个类对象的内容与我们需要构建的类对象交换即可,这里注意要自己构造交换函数,直接交换类对象可能会导致效率降低,在前面的博文中有详细介绍。
5.赋值运算符重载
//普通写法
string& operator=(const string& s)
{
if (this != &s)
{
delete[] _str;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
//现代写法
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
赋值运算符重载也不能直接用编译器生成的默认的,还是浅拷贝的问题。这里也有两种写法:一种普通写法是当不是自己给自己赋值的时候就先去释放原来的空间,然后重新申请一段跟赋值对象一样的空间大小,然后拷贝字符串,容量和长度相同。另外一种现代写法,这里传参就进行了一次拷贝构造,直接将这个拷贝构造的对象tmp和原来的对象交换即可。tmp还是临时对象出了函数就会自动销毁。
三、string类的其他成员函数
1.基础成员函数
//清除内容,容量不变
void clear()
{
_str[0] = '\0';
_size = 0;
}
//尾删函数
void pop_back()
{
--_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];
}
//容量大小
size_t capacity()const
{
return _capacity;
}
//长度大小
size_t size()const
{
return _size;
}
//返回字符串指针
const char* c_str()const
{
return _str;
}
//判空
bool empty()
{
return _size == 0;
}
这里对于这些很基础的函数实现就不做过多介绍了,相信大家都可以轻松看懂,不浪费大家时间了。
2.reserve函数
//调整容量大小
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[]_str;
_str = tmp;
_capacity = n;
}
}
这个函数非常的关键,关于容量的操作后面都是通过这个函数实现,上一篇博文也介绍了它对于不同n的处理方式,具体实现就是通过申请一个容量为n的空间,将原来的字符串拷贝进入这个空间,然后释放调原来的空间,将指针指向新申请的空间,改变容量大小即可。
3.push_back函数
void string::push_back(char ch)
{
if (_capacity == _size)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
尾插首先就得考虑容量够不够的问题,不够就需要及时的扩容。我这里选择的扩容方式是复用reserve函数,具体大小就是当容量为0扩为4,否则2倍扩容。
4.append函数
void string::append(const char* str)
{
size_t len = strlen(str);
if (len + _size > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
strcpy(_str + _size, str);
_size = len + _size;
}
和上面的尾插类似,不过这里是尾插一个字符串。还是先得考虑容量的问题,这里有一点区别先去计算插入的字符串长度,再判断容量够不够,不够的时候就需要扩容。扩容方式是先判断2倍容量够不够,不够的话就直接扩容到_size+len即可。
5.resize函数
void string::resize(size_t n, char x)
{
if (n < _size)
{
_str[n] = x;//在声明的地方x有缺省值'\0'
_size = n;
}
else
{
if (_capacity < n)
{
reserve(n);
}
for (size_t i = _size; i < n; i++)
{
_str[i] = x;
}
_size = n;
_str[_size] = '\0';
}
}
这个实现和之前我们分析的一样分n的情况讨论,这里x在声明处有缺省值,小于原来长度的时候直接截断,调整长度即可。大于原来长度还是需要考虑容量问题,容量不够直接扩容到n即可。然后将字符x依次填入,改变长度,最后别忘了加上'\0',否则会出大问题。
6.运算符重载operator+=
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
+=运算符重载也是复用了前面的append和push_back,因为都是尾插原理都是一样的,这里就不过多介绍了。
7.insert函数
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_capacity == _size)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
}
void string::insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
if (len + _size > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = s[i];
}
_size += len;
}
insert函数的实现相对复杂,因为涉及到字符串的移动,在指定位置插入的时候需要移动指定位置后面的字符串。这里单字符插入和字符串插入,实现原理类似,不过也有一点点差异。
对于单字符插入,还是得考虑容量问题,和前面尾插处理容量的方法一样,增加了一个断言是保证这里的指定位置是有效位置。然后指定位置的字符及后面的字符都向后移动一位,再将字符插入到指定位置即可。
对于字符串的插入,容量问题和append的处理一样,这里移动位置就不是移动一位,而是移动插入的字符串长度的位数。再将字符串插入进来即可。
8.erase函数
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (size_t i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
erase函数也涉及字符串的移动,不过这里不用考虑容量的问题,直接进行移动覆盖即可 。如果这里的删除的长度len大于pos后面的长度就会把后面全部删除,也就是将pos位置的值用'\0'取代即可。相反就需要将后面的往前面移动覆盖长度为len的字符串。
9.find函数
size_t string::find(char ch, size_t pos)
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t string::find(const char* str, size_t pos)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
find函数是在字符串中指定位置开始查找一个字符或者一段字符串,对于一个字符的查找用循环依次从pos位置开始查找,找到就返回下标没找到返回-1,对于字符串的查找偷懒复用了c语言中的strstr函数(针对字符串的查找),找到了就返回指向对应的字符串的指针,没找到就返回nullptr。
10.substr函数
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (len + pos > _size)
{
len = _size - pos;
}
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i];
}
return sub;
}
这里指定字符串位置和长度去构造另一个类对象,首先得确保长度len是在正确的范围内,len+pos不能超过字符串长度,超过了就及时调整。然后将类对象的容量大小提前开好就可以防止增容,提高效率。再复用+=将pos后面的len长度的字符串尾插进入sub类对象的字符串之中。返回该sub类对象即可。
11.迭代器的使用和实现
string s2("hello world");
string::iterator s = s2.begin();
while (s != s2.end())
{
cout << *s;
++s;
}
cout << endl;
for (auto ch : s2)
{
cout << ch;
}
迭代器可以有用来遍历,这两种方式不仅适用于string的遍历。对于其他容器的遍历也可以用相同的方式,因为这里的iterator可以说是一个封装,并不一定是原生指针,虽然对于string类来说这里是原生指针。起始严格来说就一种,这里第二种范围for底层也是转换为迭代器实现的。这里就相当于用指针进行依次遍历字符串一样。
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类的迭代器实现很简单就是将指针封装,用原生指针进行遍历即可。begin代表起始位置,end代表结束位置。
四、string类的非成员函数
这里的非成员函数主要是比较运算符重载,流插入运算符重载和流提取运算符重载,这里比较运算符重载写出来两个其他都复用即可,重点介绍一下流插入和流提取运算符重载。
bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator<=(const string& s1, const string& s2)
{
return s1 < s2 || s1 == s2;
}
bool operator>(const string& s1, const string& s2)
{
return !(s1 <= s2);
}
bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2);
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
比较的逻辑在上一篇博文有详细介绍,主要是按照ASCII码表比较。这里复用了c语言中的strcmp函数去比较,其他都直接或者间接的复用了实现的<和==。
1.流插入运算符重载
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
对于流插入运算符重载也比较简单,直接使用了范围for也就是迭代器去遍历字符串,然后将每个字符输出出来即可。
2.流提取运算符重载
istream& operator>>(istream& in, string& s)
{
s.clear();
const int N = 128;
char buff[N];
char ch;
ch = in.get();
int i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i] = ch;
++i;
if (i == N - 1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
对于流提取运算符重载相对来说复杂一点,这里还做了一些优化去提升效率。首先在流提取的时候如果对于有字符的string类对象,先用clear函数进行清空。为了防止扩容带来的效率降低,用到了一个数组,当字符提取的时候先提取到该数组之中,当数组满了或者遇到空格或者换行符结束循环之后再提取到类对象的字符串之中去,这就比直接+=到类对象的字符串之中去扩容次数少很多,提高了效率。这里用get或者getline去提取字符,是因为cout直接提取字符是没办法提取空格和换行符的,只有用get或者getline才能提取到空格和换行符。还有就是别忘了在末尾加'\0',不然会出大问题。
五、整体代码
1.头文件String.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace xiaoye
{
class string
{
friend istream& operator>>(istream& in, string& s);
//迭代器
public:
typedef char* iterator;
typedef const char* const_iterator;
//默认构造
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//拷贝构造
//string(const string& s)
//{
// _str = new char[s._capacity + 1];
// strcpy(_str, s._str);
// _size = s._size;
// _capacity = s._capacity;
//}
void swap(string& t)
{
std::swap(_str, t._str);
std::swap(_size, t._size);
std::swap(_capacity, t._capacity);
}
//现代写法
string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
//析构函数
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
//赋值运算符重载的现代写法
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
/*string& operator=(const string& s)
{
if (this != &s)
{
delete[] _str;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}*/
//清除内容,容量不变
void clear()
{
_str[0] = '\0';
_size = 0;
}
//尾删函数
void pop_back()
{
--_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];
}
//容量大小
size_t capacity()const
{
return _capacity;
}
//长度大小
size_t size()const
{
return _size;
}
//返回字符串指针
const char* c_str()const
{
return _str;
}
//判空
bool empty()
{
return _size == 0;
}
//可改变值迭代器,返回开始
iterator begin()
{
return _str;
}
//可改变值迭代器,返回开始
iterator end()
{
return _str + _size;
}
//不可改变值迭代器,返回开始
const_iterator begin()const
{
return _str;
}
//不可改变值迭代器,返回开始
const_iterator end()const
{
return _str + _size;
}
void reserve(size_t n);
void push_back(char ch);
void append(const char* str);
void resize(size_t n, char x = '\0');
string& operator+=(char ch);
string& operator+=(const char* str);
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
void erase(size_t pos, size_t len = npos);
size_t find(char ch, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);
string substr(size_t pos = 0, size_t len = npos);
private:
char* _str;
size_t _capacity;
size_t _size;
static const size_t npos;
};
bool operator<(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator>(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);
ostream& operator<<(ostream& out, const string& s);
istream& operator>>(istream& in, string& s);
}
2.源文件String_myself.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"string.h"
namespace xiaoye
{
const size_t string::npos = -1;
//尾插
void string::push_back(char ch)
{
if (_capacity == _size)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
void string::resize(size_t n, char x)
{
if (n < _size)
{
_str[n] = x;
_size = n;
}
else
{
if (_capacity < n)
{
reserve(n);
}
for (size_t i = _size; i < n; i++)
{
_str[i] = x;
}
_size = n;
_str[_size] = '\0';
}
}
//调整容量大小
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[]_str;
_str = tmp;
_capacity = n;
}
}
void string::append(const char* str)
{
size_t len = strlen(str);
if (len + _size > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
strcpy(_str + _size, str);
_size = len + _size;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_capacity == _size)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
}
void string::insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
if (len + _size > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = s[i];
}
_size += len;
}
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (size_t i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
size_t string::find(char ch, size_t pos)
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t string::find(const char* str, size_t pos)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (len+pos > _size)
{
len = _size - pos;
}
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i];
}
return sub;
}
bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator<=(const string& s1, const string& s2)
{
return s1 < s2 || s1 == s2;
}
bool operator>(const string& s1, const string& s2)
{
return !(s1 <= s2);
}
bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2);
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
const int N = 128;
char buff[N];
char ch;
ch = in.get();
int i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i] = ch;
++i;
if (i == N-1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
}
六、总结
仅仅了解接口的使用方法只是基础,亲手模拟实现核心接口才能真正理解其底层逻辑。希望这篇string类的模拟实现,能够帮助你对string类有一个深刻的理解。可能我的语言表述不太明确,请见谅,如果有错误的地方也请温柔的指出来谢谢。感谢您的阅读,我们一起进步,加油!

1357

被折叠的 条评论
为什么被折叠?



